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);