STC work in progress
Change-Id: Ie5e444e3b560b605b066899289cdee7a5fe8338c
diff --git a/utils/stc/bin/stc b/utils/stc/bin/stc
new file mode 100755
index 0000000..85baef0
--- /dev/null
+++ b/utils/stc/bin/stc
@@ -0,0 +1,12 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# System Test Coordinator
+#-------------------------------------------------------------------------------
+
+STC_ROOT=${STC_ROOT:-$(dirname $0)/..}
+cd $STC_ROOT
+VER=1.2.0-SNAPSHOT
+
+PATH=$PWD/bin:$PATH
+
+java -jar target/onlab-stc-$VER.jar "$@"
diff --git a/utils/stc/bin/stc-launcher b/utils/stc/bin/stc-launcher
new file mode 100755
index 0000000..3ef661e
--- /dev/null
+++ b/utils/stc/bin/stc-launcher
@@ -0,0 +1,5 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# System Test Coordinator process launcher
+#-------------------------------------------------------------------------------
+"$@" 2>&1
diff --git a/utils/stc/pom.xml b/utils/stc/pom.xml
new file mode 100644
index 0000000..0f2e496
--- /dev/null
+++ b/utils/stc/pom.xml
@@ -0,0 +1,82 @@
+<?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.2.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>
+ </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>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/utils/stc/sample/scenario.xml b/utils/stc/sample/scenario.xml
new file mode 100644
index 0000000..8cee631
--- /dev/null
+++ b/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/utils/stc/src/main/java/org/onlab/stc/Compiler.java b/utils/stc/src/main/java/org/onlab/stc/Compiler.java
new file mode 100644
index 0000000..6ee03c5
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/Compiler.java
@@ -0,0 +1,419 @@
+/*
+ * 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 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.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+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 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]";
+
+ private static final String PROP_START = "${";
+ private 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 Set<Dependency> dependencies = Sets.newHashSet();
+ private final List<Integer> parallels = Lists.newArrayList();
+
+ private ProcessFlow processFlow;
+ private File logDir;
+
+ 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);
+
+ // Produce the process flow
+ processFlow = new ProcessFlow(ImmutableSet.copyOf(steps.values()),
+ ImmutableSet.copyOf(dependencies));
+
+ // Extract the log directory if there was one specified
+ String path = scenario.definition().getString(LOG_DIR, DEFAULT_LOG_DIR);
+ 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 + ">";
+
+ // 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;
+ }
+
+ /**
+ * 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(), namespace, 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 defaultValue = parentGroup != null ? parentGroup.command() : null;
+ String command = expand(cfg.getString(COMMAND, defaultValue));
+
+ print("step name=%s command=%s", name, command);
+ Step step = new Step(name, command, 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 defaultValue = parentGroup != null ? parentGroup.command() : null;
+ String command = expand(cfg.getString(COMMAND, defaultValue));
+
+ print("group name=%s command=%s", name, command);
+ Group group = new Group(name, command, 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) {
+ 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);
+ 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);
+ 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) {
+ split(requires).forEach(name -> {
+ boolean isSoft = name.startsWith("-");
+ Step dst = getStep(expand(name.replaceFirst("^-", "")), namespace);
+ if (!inactiveSteps.containsValue(dst)) {
+ dependencies.add(new Dependency(src, dst, isSoft));
+ }
+ });
+ }
+
+ /**
+ * 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 namespace != null ? namespace + "." + name : name;
+ }
+
+ /**
+ * Expands any environment variables in the specified
+ * string. These are specified as ${property} tokens.
+ *
+ * @param string string to be processed
+ * @return original string with expanded substitutions
+ */
+ private String expand(String string) {
+ 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);
+ }
+ }
+ sb.append(value != null ? value : "");
+ last = end + 1;
+ }
+ sb.append(pString.substring(last));
+ return sb.toString();
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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/utils/stc/src/main/java/org/onlab/stc/Coordinator.java b/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
new file mode 100644
index 0000000..4ffa4af
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
@@ -0,0 +1,263 @@
+/*
+ * 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.Sets;
+
+import java.io.File;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+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 Scenario scenario;
+ private final ProcessFlow processFlow;
+
+ private final StepProcessListener delegate;
+ private final CountDownLatch latch;
+ private final ScenarioStore store;
+
+ 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
+ }
+
+ /**
+ * 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.scenario = scenario;
+ this.processFlow = processFlow;
+ this.logDir = logDir;
+ this.store = new ScenarioStore(processFlow, logDir, scenario.name());
+ this.delegate = new Delegate();
+ this.latch = new CountDownLatch(store.getSteps().size());
+ }
+
+ /**
+ * 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 the status of the specified test step.
+ *
+ * @param step test step or group
+ * @return step status
+ */
+ 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) {
+ 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 || directive == SKIP) {
+ store.updateStatus(step, IN_PROGRESS);
+ if (step instanceof Group) {
+ Group group = (Group) step;
+ delegate.onStart(group);
+ if (directive == RUN) {
+ executeRoots(group);
+ } else {
+ group.children().forEach(child -> delegate.onCompletion(child, 1));
+ }
+ } else {
+ executor.execute(new StepProcessor(step, directive == SKIP,
+ logDir, delegate));
+ }
+ }
+ }
+
+ /**
+ * 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 && !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);
+ failed = failed || status == FAILED;
+ }
+ if (done) {
+ delegate.onCompletion(group, failed ? 1 : 0);
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ listeners.forEach(listener -> listener.onStart(step));
+ }
+
+ @Override
+ public void onCompletion(Step step, int exitCode) {
+ store.updateStatus(step, exitCode == 0 ? SUCCEEDED : FAILED);
+ listeners.forEach(listener -> listener.onCompletion(step, exitCode));
+ executeSucessors(step);
+ latch.countDown();
+ }
+
+ @Override
+ public void onOutput(Step step, String line) {
+ listeners.forEach(listener -> listener.onOutput(step, line));
+ }
+
+ }
+
+}
diff --git a/utils/stc/src/main/java/org/onlab/stc/Dependency.java b/utils/stc/src/main/java/org/onlab/stc/Dependency.java
new file mode 100644
index 0000000..9025d2e
--- /dev/null
+++ b/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/utils/stc/src/main/java/org/onlab/stc/Group.java b/utils/stc/src/main/java/org/onlab/stc/Group.java
new file mode 100644
index 0000000..a094828
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/Group.java
@@ -0,0 +1,58 @@
+/*
+ * 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 group default command
+ * @param group optional group to which this step belongs
+ */
+ public Group(String name, String command, Group group) {
+ super(name, command, 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/utils/stc/src/main/java/org/onlab/stc/Main.java b/utils/stc/src/main/java/org/onlab/stc/Main.java
new file mode 100644
index 0000000..5af3817
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/Main.java
@@ -0,0 +1,139 @@
+/*
+ * 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 java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import static org.onlab.stc.Coordinator.print;
+
+/**
+ * Main program for executing system test coordinator.
+ */
+public final class Main {
+
+ private enum Command {
+ LIST, RUN, RUN_FROM, RUN_TO
+ }
+
+ private final String[] args;
+ private final Command command;
+ private final String scenarioFile;
+
+ private Scenario scenario;
+ private Coordinator coordinator;
+ private Listener delegate = new Listener();
+
+ // Public construction forbidden
+ private Main(String[] args) {
+ this.args = args;
+ this.scenarioFile = args[0];
+ this.command = Command.valueOf("RUN");
+ }
+
+ // usage: stc [<command>] [<scenario-file>]
+ // --list
+ // [--run]
+ // --run-from <step>,...
+ // --run-to <step>,...
+
+ /**
+ * 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();
+ }
+
+ private void run() {
+ try {
+ // Load scenario
+ scenario = Scenario.loadScenario(new FileInputStream(scenarioFile));
+
+ // Elaborate scenario
+ Compiler compiler = new Compiler(scenario);
+ compiler.compile();
+
+ // Execute process flow
+ coordinator = new Coordinator(scenario, compiler.processFlow(),
+ compiler.logDir());
+ coordinator.addListener(delegate);
+ processCommand();
+
+ } catch (FileNotFoundException e) {
+ print("Unable to find scenario file %s", scenarioFile);
+ }
+ }
+
+ private void processCommand() {
+ switch (command) {
+ case RUN:
+ processRun();
+ default:
+ print("Unsupported command");
+ }
+ }
+
+ private void processRun() {
+ try {
+ coordinator.start();
+ int exitCode = coordinator.waitFor();
+ pause(100); // allow stdout to flush
+ System.exit(exitCode);
+ } catch (InterruptedException e) {
+ print("Unable to execute scenario %s", scenarioFile);
+ }
+ }
+
+ private void pause(int ms) {
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ print("Interrupted!");
+ }
+ }
+
+
+ /**
+ * Internal delegate to monitor the process execution.
+ */
+ private class Listener implements StepProcessListener {
+
+ @Override
+ public void onStart(Step step) {
+ print("%s %s started", now(), step.name());
+ }
+
+ @Override
+ public void onCompletion(Step step, int exitCode) {
+ print("%s %s %s", now(), step.name(), exitCode == 0 ? "completed" : "failed");
+ }
+
+ @Override
+ public void onOutput(Step step, String line) {
+ }
+
+ }
+
+ private String now() {
+ return new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date());
+ }
+
+}
diff --git a/utils/stc/src/main/java/org/onlab/stc/ProcessFlow.java b/utils/stc/src/main/java/org/onlab/stc/ProcessFlow.java
new file mode 100644
index 0000000..4d99b33
--- /dev/null
+++ b/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/utils/stc/src/main/java/org/onlab/stc/Scenario.java b/utils/stc/src/main/java/org/onlab/stc/Scenario.java
new file mode 100644
index 0000000..fd2cd62
--- /dev/null
+++ b/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/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java b/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java
new file mode 100644
index 0000000..22ca452
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java
@@ -0,0 +1,120 @@
+/*
+ * 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.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.Map;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.stc.Coordinator.Status.FAILED;
+import static org.onlab.stc.Coordinator.Status.WAITING;
+import static org.onlab.stc.Coordinator.print;
+
+/**
+ * Maintains state of scenario execution.
+ */
+class ScenarioStore {
+
+ private final ProcessFlow processFlow;
+ private final File storeFile;
+
+ private final Map<Step, Status> stepStatus = 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.storeFile = new File(logDir, name + ".stc");
+ processFlow.getVertexes().forEach(step -> stepStatus.put(step, WAITING));
+ }
+
+
+ /**
+ * Returns set of all test steps.
+ *
+ * @return set of steps
+ */
+ Set<Step> getSteps() {
+ return processFlow.getVertexes();
+ }
+
+ /**
+ * Returns the status of the specified test step.
+ *
+ * @param step test step or group
+ * @return step status
+ */
+ Status getStatus(Step step) {
+ return checkNotNull(stepStatus.get(step), "Step %s not found", step.name());
+ }
+
+ /**
+ * Updates the status of the specified test step.
+ *
+ * @param step test step or group
+ * @param status new step status
+ */
+ void updateStatus(Step step, Status status) {
+ stepStatus.put(step, status);
+ save();
+ }
+
+ /**
+ * Indicates whether there are any failures.
+ *
+ * @return true if there are failed steps
+ */
+ boolean hasFailures() {
+ for (Status status : stepStatus.values()) {
+ if (status == FAILED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Loads the states from disk.
+ */
+ private void load() {
+ // FIXME: implement this
+ }
+
+ /**
+ * Saves the states to disk.
+ */
+ private void save() {
+ try {
+ PropertiesConfiguration cfg = new PropertiesConfiguration(storeFile);
+ stepStatus.forEach((step, status) -> cfg.setProperty(step.name(), status));
+ cfg.save();
+ } catch (ConfigurationException e) {
+ print("Unable to store file %s", storeFile);
+ }
+ }
+
+}
diff --git a/utils/stc/src/main/java/org/onlab/stc/Step.java b/utils/stc/src/main/java/org/onlab/stc/Step.java
new file mode 100644
index 0000000..834e0ec
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/Step.java
@@ -0,0 +1,103 @@
+/*
+ * 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 Group group;
+
+ /**
+ * Creates a new test step.
+ *
+ * @param name step name
+ * @param command step command to execute
+ * @param group optional group to which this step belongs
+ */
+ public Step(String name, String command, Group group) {
+ this.name = checkNotNull(name, "Name cannot be null");
+ this.group = group;
+
+ // Set the command; if one is not given default to the enclosing group
+ this.command = command != null ? command :
+ group != null && group.command != null ? group.command : 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 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("group", group)
+ .toString();
+ }
+}
diff --git a/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java b/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java
new file mode 100644
index 0000000..2c751a1
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java
@@ -0,0 +1,49 @@
+/*
+ * 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
+ */
+ default void onStart(Step step) {
+ }
+
+ /**
+ * Indicates that process step has completed.
+ *
+ * @param step subject step
+ * @param exitCode step process exit exitCode
+ */
+ default void onCompletion(Step step, int exitCode) {
+ }
+
+ /**
+ * 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/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java b/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java
new file mode 100644
index 0000000..b2d7635
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java
@@ -0,0 +1,120 @@
+/*
+ * 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 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 org.onlab.stc.Coordinator.print;
+
+/**
+ * Manages execution of the specified step or a group.
+ */
+class StepProcessor implements Runnable {
+
+ private static final int FAIL = -1;
+
+ static String launcher = "stc-launcher ";
+
+ private final Step step;
+ private final boolean skip;
+ private final File logDir;
+
+ private Process process;
+ private StepProcessListener delegate;
+
+ /**
+ * Creates a process monitor.
+ *
+ * @param step step or group to be executed
+ * @param skip indicates the process should not actually execute
+ * @param logDir directory where step process log should be stored
+ * @param delegate process lifecycle listener
+ */
+ StepProcessor(Step step, boolean skip, File logDir, StepProcessListener delegate) {
+ this.step = step;
+ this.skip = skip;
+ this.logDir = logDir;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void run() {
+ int code = FAIL;
+ delegate.onStart(step);
+ if (!skip) {
+ code = execute();
+ }
+ delegate.onCompletion(step, code);
+ }
+
+ /**
+ * Executes the step process.
+ *
+ * @return exit code
+ */
+ private int execute() {
+ try (PrintWriter pw = new PrintWriter(logFile(step))) {
+ process = Runtime.getRuntime().exec(launcher + step.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;
+ }
+
+ /**
+ * 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 specified step.
+ *
+ * @param step test step
+ * @return log file
+ */
+ private File logFile(Step step) {
+ return new File(logDir, step.name() + ".log");
+ }
+
+}
diff --git a/utils/stc/src/main/java/org/onlab/stc/package-info.java b/utils/stc/src/main/java/org/onlab/stc/package-info.java
new file mode 100644
index 0000000..5614589
--- /dev/null
+++ b/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/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java b/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java
new file mode 100644
index 0000000..7346b0a
--- /dev/null
+++ b/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 org.junit.BeforeClass;
+import org.junit.Test;
+import org.onlab.util.Tools;
+
+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 = new File("/tmp/junit-stc");
+
+ @BeforeClass
+ public static void setUpClass() throws IOException {
+ Tools.removeDirectory(TEST_DIR);
+ TEST_DIR.mkdirs();
+ 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("OC1", "1.2.3.1");
+ System.setProperty("OC2", "1.2.3.2");
+ System.setProperty("OC3", "1.2.3.3");
+ }
+
+ public static FileInputStream getStream(String name) throws FileNotFoundException {
+ return new FileInputStream(new File(TEST_DIR, name));
+ }
+
+ private 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", 25, flow.getVertexes().size());
+ assertEquals("incorrect dependency count", 21, flow.getEdges().size());
+ assertEquals("incorrect logDir", "/tmp/junit-stc/foo", compiler.logDir().getPath());
+
+ 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/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java b/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
new file mode 100644
index 0000000..0df4f68
--- /dev/null
+++ b/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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 java.io.FileNotFoundException;
+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();
+ StepProcessor.launcher = "true ";
+ }
+
+ @Test
+ public void simple() throws FileNotFoundException, InterruptedException {
+ executeTest("simple-scenario.xml");
+ }
+
+ @Test
+ public void complex() throws FileNotFoundException, InterruptedException {
+ executeTest("scenario.xml");
+ }
+
+ private void executeTest(String name) throws FileNotFoundException, InterruptedException {
+ Scenario scenario = loadScenario(getStream(name));
+ Compiler compiler = new Compiler(scenario);
+ compiler.compile();
+ coordinator = new Coordinator(scenario, compiler.processFlow(), compiler.logDir());
+ coordinator.addListener(listener);
+ coordinator.start();
+ coordinator.waitFor();
+ coordinator.removeListener(listener);
+ }
+
+ private class Listener implements StepProcessListener {
+ @Override
+ public void onStart(Step step) {
+ print("> %s: started", step.name());
+ }
+
+ @Override
+ public void onCompletion(Step step, int exitCode) {
+ print("< %s: %s", step.name(), exitCode == 0 ? "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/utils/stc/src/test/java/org/onlab/stc/DependencyTest.java b/utils/stc/src/test/java/org/onlab/stc/DependencyTest.java
new file mode 100644
index 0000000..7d56892
--- /dev/null
+++ b/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);
+ step2 = new Step("step2", CMD, 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/utils/stc/src/test/java/org/onlab/stc/GroupTest.java b/utils/stc/src/test/java/org/onlab/stc/GroupTest.java
new file mode 100644
index 0000000..8062fe5
--- /dev/null
+++ b/utils/stc/src/test/java/org/onlab/stc/GroupTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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, parent);
+ assertEquals("incorrect name", NAME, group.name());
+ assertEquals("incorrect command", CMD, group.command());
+ assertSame("incorrect group", parent, group.group());
+
+ Step step = new Step("step", null, group);
+ group.addChild(step);
+ assertSame("incorrect child", step, group.children().iterator().next());
+ }
+
+ @Test
+ public void equality() {
+ Group g1 = new Group(NAME, CMD, parent);
+ Group g2 = new Group(NAME, CMD, null);
+ Group g3 = new Group("foo", null, parent);
+ new EqualsTester()
+ .addEqualityGroup(g1, g2)
+ .addEqualityGroup(g3)
+ .testEquals();
+ }
+
+}
\ No newline at end of file
diff --git a/utils/stc/src/test/java/org/onlab/stc/ScenarioTest.java b/utils/stc/src/test/java/org/onlab/stc/ScenarioTest.java
new file mode 100644
index 0000000..2aa5174
--- /dev/null
+++ b/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/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java b/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java
new file mode 100644
index 0000000..0a932a4
--- /dev/null
+++ b/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onlab.util.Tools;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test of the step processor.
+ */
+public class StepProcessorTest {
+
+ private static final File DIR = new File("/tmp/stc/foo");
+
+ private final Listener delegate = new Listener();
+
+ @BeforeClass
+ public static void setUpClass() {
+ StepProcessor.launcher = "";
+ DIR.mkdirs();
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws IOException {
+ Tools.removeDirectory(DIR.getPath());
+ }
+
+ @Test
+ public void executed() {
+ Step step = new Step("foo", "ls /tmp", null);
+ StepProcessor processor = new StepProcessor(step, false, DIR, delegate);
+ processor.run();
+ assertTrue("should be started", delegate.started);
+ assertTrue("should have output", delegate.output);
+ assertTrue("should be stopped", delegate.stopped);
+ assertEquals("incorrect code", 0, delegate.code);
+ }
+
+
+ @Test
+ public void skipped() {
+ Step step = new Step("foo", "ls /tmp", null);
+ StepProcessor processor = new StepProcessor(step, true, DIR, delegate);
+ processor.run();
+ assertTrue("should be started", delegate.started);
+ assertFalse("should have output", delegate.output);
+ assertTrue("should be stopped", delegate.stopped);
+ assertEquals("incorrect code", -1, delegate.code);
+ }
+
+ private class Listener implements StepProcessListener {
+
+ private int code = 123;
+ private boolean started, stopped, output;
+
+ @Override
+ public void onStart(Step step) {
+ started = true;
+ }
+
+ @Override
+ public void onCompletion(Step step, int exitCode) {
+ stopped = true;
+ this.code = exitCode;
+ }
+
+ @Override
+ public void onOutput(Step step, String line) {
+ output = true;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/utils/stc/src/test/java/org/onlab/stc/StepTest.java b/utils/stc/src/test/java/org/onlab/stc/StepTest.java
new file mode 100644
index 0000000..6b3ee79
--- /dev/null
+++ b/utils/stc/src/test/java/org/onlab/stc/StepTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 Group parent;
+
+ @Before
+ public void setUp() throws ConfigurationException {
+ parent = new Group("parent", null, null);
+ }
+
+ @Test
+ public void basics() {
+ Step step = new Step(NAME, CMD, parent);
+ assertEquals("incorrect name", NAME, step.name());
+ assertEquals("incorrect command", CMD, step.command());
+ assertSame("incorrect group", parent, step.group());
+ }
+
+ @Test
+ public void equality() {
+ Step s1 = new Step(NAME, CMD, parent);
+ Step s2 = new Step(NAME, CMD, null);
+ Step s3 = new Step("foo", null, parent);
+ new EqualsTester()
+ .addEqualityGroup(s1, s2)
+ .addEqualityGroup(s3)
+ .testEquals();
+ }
+}
\ No newline at end of file
diff --git a/utils/stc/src/test/resources/org/onlab/stc/one-scenario.xml b/utils/stc/src/test/resources/org/onlab/stc/one-scenario.xml
new file mode 100644
index 0000000..e5cb6f2
--- /dev/null
+++ b/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/utils/stc/src/test/resources/org/onlab/stc/scenario.xml b/utils/stc/src/test/resources/org/onlab/stc/scenario.xml
new file mode 100644
index 0000000..3a566fd
--- /dev/null
+++ b/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="/tmp/junit-stc/foo">
+ <import file="/tmp/junit-stc/one-scenario.xml" namespace="foo"/>
+
+ <import file="/tmp/junit-stc/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="${OC#}" requires="alpha">
+ <step name="ping-${#}" exec="asdads ${OC#}"/>
+ <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/utils/stc/src/test/resources/org/onlab/stc/simple-scenario.xml b/utils/stc/src/test/resources/org/onlab/stc/simple-scenario.xml
new file mode 100644
index 0000000..c70fe87
--- /dev/null
+++ b/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/utils/stc/src/test/resources/org/onlab/stc/two-scenario.xml b/utils/stc/src/test/resources/org/onlab/stc/two-scenario.xml
new file mode 100644
index 0000000..0d6135d
--- /dev/null
+++ b/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