Thomas Vachuska

Enhancing STC

Change-Id: If9429cc6a30333bd27b579825fe6b1fac221cf60
......@@ -13,4 +13,8 @@ scenario=${1:-smoke}
[ ! -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 "$@"
......
......@@ -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 @@ public class Coordinator {
private final ExecutorService executor = newFixedThreadPool(MAX_THREADS);
private final Scenario scenario;
private final ProcessFlow processFlow;
private final StepProcessListener delegate;
......@@ -57,7 +58,7 @@ public class Coordinator {
* Represents processor state.
*/
public enum Status {
WAITING, IN_PROGRESS, SUCCEEDED, FAILED
WAITING, IN_PROGRESS, SUCCEEDED, FAILED, SKIPPED
}
/**
......@@ -68,7 +69,6 @@ public class Coordinator {
* @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 @@ public class Coordinator {
}
/**
* 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 @@ public class Coordinator {
}
/**
* 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 @@ public class Coordinator {
* @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 @@ public class Coordinator {
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 @@ public class Coordinator {
@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();
......
......@@ -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 @@ import static org.onlab.stc.Coordinator.print;
*/
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 @@ public final class Main {
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 @@ public final class Main {
}
}
// 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 @@ public final class Main {
}
}
private void pause(int ms) {
// 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 {
Thread.sleep(ms);
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("Interrupted!");
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) {
}
}
// 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")));
}
private String now() {
return new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date());
// 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!");
}
}
}
......
......@@ -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 @@ class ScenarioStore {
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 @@ class ScenarioStore {
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 @@ class ScenarioStore {
}
/**
* Returns the status of the specified test step.
* Returns a chronological list of step or group records.
*
* @return list of events
*/
synchronized List<StepEvent> getEvents() {
return ImmutableList.copyOf(events);
}
/**
* Returns the status record of the specified test step.
*
* @param step test step or group
* @return step status
* @return step status record
*/
Status getStatus(Step step) {
return checkNotNull(stepStatus.get(step), "Step %s not found", step.name());
return checkNotNull(statusMap.get(step.name()), "Step %s not found", step.name());
}
/**
* Updates the status of the specified test step.
* 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 @@ class ScenarioStore {
* @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 @@ class ScenarioStore {
}
/**
* 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 @@ class ScenarioStore {
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);
......
/*
* 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]));
}
}
......@@ -55,6 +55,7 @@ public class CoordinatorTest {
compiler.compile();
coordinator = new Coordinator(scenario, compiler.processFlow(), compiler.logDir());
coordinator.addListener(listener);
coordinator.reset();
coordinator.start();
coordinator.waitFor();
coordinator.removeListener(listener);
......