Enhancing STC
Change-Id: If9429cc6a30333bd27b579825fe6b1fac221cf60
diff --git a/tools/test/bin/stc b/tools/test/bin/stc
index fd201b6..ba956b4 100755
--- a/tools/test/bin/stc
+++ b/tools/test/bin/stc
@@ -13,4 +13,8 @@
[ ! -f $scenario ] && scenario=$scenario.xml
[ ! -f $scenario ] && echo "Scenario $scenario file not found" && exit 1
-java -jar $JAR $scenario
+[ $# -ge 1 ] && shift
+
+[ -t 1 ] && stcColor=true || unset stcColor
+
+java -jar $JAR $scenario "$@"
diff --git a/utils/stc/src/main/java/org/onlab/stc/Coordinator.java b/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
index 4ffa4af..209de84 100644
--- a/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
+++ b/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
@@ -15,9 +15,11 @@
*/
package org.onlab.stc;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import java.io.File;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
@@ -36,7 +38,6 @@
private final ExecutorService executor = newFixedThreadPool(MAX_THREADS);
- private final Scenario scenario;
private final ProcessFlow processFlow;
private final StepProcessListener delegate;
@@ -57,7 +58,7 @@
* Represents processor state.
*/
public enum Status {
- WAITING, IN_PROGRESS, SUCCEEDED, FAILED
+ WAITING, IN_PROGRESS, SUCCEEDED, FAILED, SKIPPED
}
/**
@@ -68,7 +69,6 @@
* @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());
@@ -77,6 +77,46 @@
}
/**
+ * 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() {
@@ -104,10 +144,19 @@
}
/**
- * Returns the status of the specified test step.
+ * 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
+ * @return step status record
*/
public Status getStatus(Step step) {
return store.getStatus(step);
@@ -138,6 +187,7 @@
* @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 -> {
@@ -155,7 +205,7 @@
private synchronized void execute(Step step) {
Directive directive = nextAction(step);
if (directive == RUN || directive == SKIP) {
- store.updateStatus(step, IN_PROGRESS);
+ store.markStarted(step);
if (step instanceof Group) {
Group group = (Group) step;
delegate.onStart(group);
@@ -247,7 +297,7 @@
@Override
public void onCompletion(Step step, int exitCode) {
- store.updateStatus(step, exitCode == 0 ? SUCCEEDED : FAILED);
+ store.markComplete(step, exitCode == 0 ? SUCCEEDED : FAILED);
listeners.forEach(listener -> listener.onCompletion(step, exitCode));
executeSucessors(step);
latch.countDown();
diff --git a/utils/stc/src/main/java/org/onlab/stc/Main.java b/utils/stc/src/main/java/org/onlab/stc/Main.java
index 5af3817..567aaa6 100644
--- a/utils/stc/src/main/java/org/onlab/stc/Main.java
+++ b/utils/stc/src/main/java/org/onlab/stc/Main.java
@@ -15,11 +15,18 @@
*/
package org.onlab.stc;
+import com.google.common.collect.ImmutableList;
+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 static java.lang.System.currentTimeMillis;
+import static org.onlab.stc.Coordinator.Status.*;
import static org.onlab.stc.Coordinator.print;
/**
@@ -27,30 +34,53 @@
*/
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 enum Command {
- LIST, RUN, RUN_FROM, RUN_TO
+ LIST, RUN, RUN_RANGE, HELP
}
- private final String[] args;
- private final Command command;
private final String scenarioFile;
- private Scenario scenario;
+ private Command command = Command.HELP;
+ private String runFromPatterns = "";
+ private String runToPatterns = "";
+
private Coordinator coordinator;
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.args = args;
this.scenarioFile = args[0];
- this.command = Command.valueOf("RUN");
- }
- // usage: stc [<command>] [<scenario-file>]
- // --list
- // [--run]
- // --run-from <step>,...
- // --run-to <step>,...
+ 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.
@@ -62,10 +92,11 @@
main.run();
}
+ // Runs the scenario processing
private void run() {
try {
// Load scenario
- scenario = Scenario.loadScenario(new FileInputStream(scenarioFile));
+ Scenario scenario = Scenario.loadScenario(new FileInputStream(scenarioFile));
// Elaborate scenario
Compiler compiler = new Compiler(scenario);
@@ -82,17 +113,27 @@
}
}
+ // 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");
+ print("Unsupported command %s", command);
}
}
+ // Processes the scenario 'run' command.
private void processRun() {
try {
+ coordinator.reset();
coordinator.start();
int exitCode = coordinator.waitFor();
pause(100); // allow stdout to flush
@@ -102,38 +143,85 @@
}
}
- private void pause(int ms) {
- try {
- Thread.sleep(ms);
- } catch (InterruptedException e) {
- print("Interrupted!");
- }
+ // Processes the scenario 'list' command.
+ private void processList() {
+ coordinator.getRecords()
+ .forEach(event -> logStatus(event.time(), event.name(), event.status()));
}
+ // Processes the scenario 'run' command for range of steps.
+ private void processRunRange() {
+ try {
+ coordinator.reset(list(runFromPatterns), list(runToPatterns));
+ 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);
+ }
+ }
/**
* Internal delegate to monitor the process execution.
*/
- private class Listener implements StepProcessListener {
-
+ private static class Listener implements StepProcessListener {
@Override
public void onStart(Step step) {
- print("%s %s started", now(), step.name());
+ logStatus(currentTimeMillis(), step.name(), IN_PROGRESS);
}
@Override
public void onCompletion(Step step, int exitCode) {
- print("%s %s %s", now(), step.name(), exitCode == 0 ? "completed" : "failed");
+ logStatus(currentTimeMillis(), step.name(), exitCode == 0 ? SUCCEEDED : FAILED);
}
@Override
public void onOutput(Step step, String line) {
}
-
}
- private String now() {
- return new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date());
+ // Logs the step status.
+ private static void logStatus(long time, String name, Status status) {
+ 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!");
+ }
}
}
diff --git a/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java b/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java
index 22ca452..614afb1 100644
--- a/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java
+++ b/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java
@@ -15,18 +15,20 @@
*/
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.FAILED;
-import static org.onlab.stc.Coordinator.Status.WAITING;
+import static org.onlab.stc.Coordinator.Status.*;
import static org.onlab.stc.Coordinator.print;
/**
@@ -37,7 +39,8 @@
private final ProcessFlow processFlow;
private final File storeFile;
- private final Map<Step, Status> stepStatus = Maps.newConcurrentMap();
+ 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.
@@ -49,9 +52,25 @@
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));
+ 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 {
+ 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.
@@ -63,23 +82,42 @@
}
/**
- * Returns the status of the specified test step.
+ * Returns a chronological list of step or group records.
*
- * @param step test step or group
- * @return step status
+ * @return list of events
*/
- Status getStatus(Step step) {
- return checkNotNull(stepStatus.get(step), "Step %s not found", step.name());
+ synchronized List<StepEvent> getEvents() {
+ return ImmutableList.copyOf(events);
}
/**
- * Updates the status of the specified test step.
+ * 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));
+ save();
+ }
+
+ /**
+ * Marks the specified test step as being complete.
*
* @param step test step or group
* @param status new step status
*/
- void updateStatus(Step step, Status status) {
- stepStatus.put(step, status);
+ synchronized void markComplete(Step step, Status status) {
+ add(new StepEvent(step.name(), status));
save();
}
@@ -89,7 +127,7 @@
* @return true if there are failed steps
*/
boolean hasFailures() {
- for (Status status : stepStatus.values()) {
+ for (Status status : statusMap.values()) {
if (status == FAILED) {
return true;
}
@@ -98,10 +136,26 @@
}
/**
+ * 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() {
- // FIXME: implement this
+ try {
+ PropertiesConfiguration cfg = new PropertiesConfiguration(storeFile);
+ cfg.getKeys().forEachRemaining(prop -> add(StepEvent.fromString(cfg.getString(prop))));
+ cfg.save();
+ } catch (ConfigurationException e) {
+ print("Unable to store file %s", storeFile);
+ }
}
/**
@@ -110,7 +164,7 @@
private void save() {
try {
PropertiesConfiguration cfg = new PropertiesConfiguration(storeFile);
- stepStatus.forEach((step, status) -> cfg.setProperty(step.name(), status));
+ events.forEach(event -> cfg.setProperty("T" + event.time(), event.toString()));
cfg.save();
} catch (ConfigurationException e) {
print("Unable to store file %s", storeFile);
diff --git a/utils/stc/src/main/java/org/onlab/stc/StepEvent.java b/utils/stc/src/main/java/org/onlab/stc/StepEvent.java
new file mode 100644
index 0000000..4c10f23
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/StepEvent.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+/**
+ * Represents an event of execution of a scenario step or group.
+ */
+public class StepEvent {
+
+ private final String name;
+ private final long time;
+ private final Status status;
+
+ /**
+ * 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
+ */
+ public StepEvent(String name, long time, Status status) {
+ this.name = name;
+ this.time = time;
+ this.status = status;
+ }
+
+ /**
+ * Creates a new step record for non-running status.
+ *
+ * @param name test step or group name
+ * @param status status
+ */
+ public StepEvent(String name, Status status) {
+ this(name, System.currentTimeMillis(), status);
+ }
+
+ /**
+ * 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;
+ }
+
+
+ @Override
+ public String toString() {
+ return name + ":" + time + ":" + status;
+ }
+
+ /**
+ * 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 new StepEvent(fields[0], parseLong(fields[1]), Status.valueOf(fields[2]));
+ }
+}
diff --git a/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java b/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
index 0df4f68..67655f1 100644
--- a/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
+++ b/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
@@ -55,6 +55,7 @@
compiler.compile();
coordinator = new Coordinator(scenario, compiler.processFlow(), compiler.logDir());
coordinator.addListener(listener);
+ coordinator.reset();
coordinator.start();
coordinator.waitFor();
coordinator.removeListener(listener);