Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #207 UsageFormatter #214

Merged
merged 10 commits into from
Mar 26, 2012
14 changes: 14 additions & 0 deletions core/src/main/java/cucumber/formatter/FormatterFactory.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package cucumber.formatter;

import cucumber.formatter.usage.AverageUsageStatisticStrategy;
import cucumber.formatter.usage.MedianUsageStatisticStrategy;
import cucumber.runtime.CucumberException;
import gherkin.formatter.Formatter;
import gherkin.formatter.JSONFormatter;
Expand All @@ -8,6 +10,7 @@

import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -21,6 +24,7 @@ public class FormatterFactory {
put("json", JSONFormatter.class.getName());
put("json-pretty", JSONPrettyFormatter.class.getName());
put("pretty", PrettyFormatter.class.getName());
put("usage", UsageFormatter.class.getName());
}};

public FormatterFactory(ClassLoader classLoader) {
Expand Down Expand Up @@ -51,6 +55,8 @@ private Formatter createFormatterFromClassName(String className, Object out) {
return formatterClass.getConstructor(ctorArgClass, Boolean.TYPE, Boolean.TYPE).newInstance(out, false, true);
} else if (ProgressFormatter.class.isAssignableFrom(formatterClass)) {
return formatterClass.getConstructor(ctorArgClass, Boolean.TYPE).newInstance(out, false);
} else if (UsageFormatter.class.isAssignableFrom(formatterClass)) {
return createUsageFormatter(out, ctorArgClass, formatterClass);
} else {
return formatterClass.getConstructor(ctorArgClass).newInstance(out);
}
Expand All @@ -66,4 +72,12 @@ private Class<Formatter> getFormatterClass(String className) {
throw new CucumberException("Formatter class not found: " + className, e);
}
}

private UsageFormatter createUsageFormatter(Object out, Class ctorArgClass, Class<Formatter> formatterClass) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException
{
UsageFormatter formatter = (UsageFormatter)formatterClass.getConstructor(ctorArgClass).newInstance(out);
formatter.addUsageStatisticStrategy("average", new AverageUsageStatisticStrategy());
formatter.addUsageStatisticStrategy("median", new MedianUsageStatisticStrategy());
return formatter;
}
}
255 changes: 255 additions & 0 deletions core/src/main/java/cucumber/formatter/UsageFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package cucumber.formatter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import cucumber.formatter.usage.UsageStatisticStrategy;
import gherkin.deps.com.google.gson.Gson;
import gherkin.deps.com.google.gson.GsonBuilder;
import gherkin.formatter.Format;
import gherkin.formatter.Formatter;
import gherkin.formatter.MonochromeFormats;
import gherkin.formatter.NiceAppendable;
import gherkin.formatter.Reporter;
import gherkin.formatter.StepPrinter;
import gherkin.formatter.model.Background;
import gherkin.formatter.model.Examples;
import gherkin.formatter.model.Feature;
import gherkin.formatter.model.Match;
import gherkin.formatter.model.Result;
import gherkin.formatter.model.Scenario;
import gherkin.formatter.model.ScenarioOutline;
import gherkin.formatter.model.Step;

/**
* Formatter to measure performance of steps. Aggregated results for all steps can be computed
* by adding {@link UsageStatisticStrategy} to the usageFormatter
*/
public class UsageFormatter implements Formatter, Reporter
{
private final MonochromeFormats monochromeFormat = new MonochromeFormats();
private final StepPrinter stepPrinter = new StepPrinter();

final Map<String, List<Long>> usageMap = new HashMap<String, List<Long>>();
final Map<String, UsageStatisticStrategy> statisticStrategies = new HashMap<String, UsageStatisticStrategy>();

private final List<Step> steps = new ArrayList<Step>();
private final NiceAppendable out;

private Match match;

/**
* Constructor
* @param out {@link Appendable} to print the result
*/
public UsageFormatter(Appendable out)
{
this.out = new NiceAppendable(out);
}

@Override
public void uri(String uri)
{
}

@Override
public void feature(Feature feature)
{
}

@Override
public void background(Background background)
{
}

@Override
public void scenario(Scenario scenario)
{
}

@Override
public void scenarioOutline(ScenarioOutline scenarioOutline)
{
}

@Override
public void examples(Examples examples)
{
}

@Override
public void step(Step step)
{
steps.add(step);
}

@Override
public void eof()
{
}

@Override
public void syntaxError(String state, String event, List<String> legalEvents, String uri, int line)
{
}

@Override
public void done()
{
List<StepContainer> stepContainers = new ArrayList<StepContainer>();

for (Map.Entry<String, List<Long>> usageEntry : usageMap.entrySet())
{
StepContainer stepContainer = new StepContainer();
stepContainers.add(stepContainer);

stepContainer.stepName = usageEntry.getKey();
stepContainer.durations = formatDurationEntries(usageEntry.getValue());

stepContainer.aggregatedResults = createAggregatedResults(usageEntry);
}

out.append(gson().toJson(stepContainers));
}

private List<String> formatDurationEntries(List<Long> durationEntries)
{
ArrayList<String> formattedDuration = new ArrayList<String>();
for(Long duration : durationEntries)
{
formattedDuration.add(formatDuration(duration));
}
return formattedDuration;
}

private List<AggregatedResult> createAggregatedResults(Map.Entry<String, List<Long>> usageEntry)
{
ArrayList<AggregatedResult> aggregatedResults = new ArrayList<AggregatedResult>();
for (Map.Entry<String, UsageStatisticStrategy> calculatorEntry : statisticStrategies.entrySet())
{
AggregatedResult aggregatedResult = new AggregatedResult();
aggregatedResults.add(aggregatedResult);

UsageStatisticStrategy statisticStrategy = calculatorEntry.getValue();
Long calculationResult = statisticStrategy.calculate(usageEntry.getValue());

aggregatedResult.strategy = calculatorEntry.getKey();
aggregatedResult.value = formatDuration(calculationResult);
}
return aggregatedResults;
}

private String formatDuration(Long duration)
{
long seconds = TimeUnit.MICROSECONDS.toSeconds(duration);
long microSeconds = duration - TimeUnit.SECONDS.toMicros(seconds);
return String.format("%d.%06d", seconds, microSeconds);
}

private Gson gson() {
return new GsonBuilder().setPrettyPrinting().create();
}

@Override
public void close()
{
out.close();
}

@Override
public void result(Result result)
{
if (!steps.isEmpty())
{
Step step = steps.remove(0);
String stepNameWithArgs = formatStepNameWithArgs(result, step);
addUsageEntry(result, stepNameWithArgs);
}
}

private String formatStepNameWithArgs(Result result, Step step)
{
StringBuffer buffer = new StringBuffer();
buffer.append(step.getKeyword()).append(" ");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyword already contains a space, so son't add an extra one.

Format format = getFormat(result.getStatus());
Format argFormat = getArgFormat(result.getStatus());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Format is for ANSI-coloured output. Did you really intend to put colours in the JSON?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did use the MonochromeFormat in order to reuse the StepPrinter for printing Steps and their corresponding arguments.

stepPrinter.writeStep(new NiceAppendable(buffer), format, argFormat, step.getName(), match.getArguments());

return buffer.toString();
}

private void addUsageEntry(Result result, String stepNameWithArgs)
{
List<Long> durationEntries = usageMap.get(stepNameWithArgs);
if (durationEntries == null)
{
durationEntries = new ArrayList<Long>();
usageMap.put(stepNameWithArgs, durationEntries);
}
durationEntries.add(durationInMillis(result));
}

private Long durationInMillis(Result result)
{
long duration;
if (result.getDuration() == null)
{
duration = 0;
} else
{
duration = result.getDuration() / 1000;
}
return duration;
}

@Override
public void match(Match match)
{
this.match = match;
}

@Override
public void embedding(String mimeType, byte[] data)
{
}

private Format getFormat(String key) {
return monochromeFormat.get(key);
}

private Format getArgFormat(String key) {
return monochromeFormat.get(key + "_arg");
}

/**
* Add a {@link UsageStatisticStrategy} to the formatter
* @param key the key, will be displayed in the output
* @param strategy the strategy
*/
public void addUsageStatisticStrategy(String key, UsageStatisticStrategy strategy)
{
statisticStrategies.put(key, strategy);
}

/**
* Contains for usage-entries of steps
*/
private static class StepContainer {
public String stepName;
public List<AggregatedResult> aggregatedResults = new ArrayList<AggregatedResult>();
public List<String> durations = new ArrayList<String>();
}

/**
* Container for aggregated results, computed by a specific strategy (e.g. average, median, ..)
*/
private static class AggregatedResult {
public String strategy;
public String value;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cucumber.formatter.usage;

import java.util.List;

/**
* Calculate the average of a list of duration entries
*/
public class AverageUsageStatisticStrategy implements UsageStatisticStrategy
{
@Override
public Long calculate(List<Long> durationEntries)
{
if (verifyNoNulls(durationEntries))
{
return 0L;
}

long sum = 0;
for (Long duration : durationEntries)
{
sum += duration;
}
return sum / durationEntries.size();
}

private boolean verifyNoNulls(List<Long> durationEntries)
{
return durationEntries == null || durationEntries.isEmpty() || durationEntries.contains(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cucumber.formatter.usage;

import java.util.Collections;
import java.util.List;

/**
* Calculate the median of a list of duration entries
*/
public class MedianUsageStatisticStrategy implements UsageStatisticStrategy
{
@Override
public Long calculate(List<Long> durationEntries)
{
if (verifyNoNulls(durationEntries))
{
return 0L;
}
Collections.sort(durationEntries);
int middle = durationEntries.size() / 2;
if (durationEntries.size() % 2 == 1)
{
return durationEntries.get(middle);
}
else
{
return (durationEntries.get(middle - 1) + durationEntries.get(middle)) / 2;
}
}

private boolean verifyNoNulls(List<Long> durationEntries)
{
return durationEntries == null || durationEntries.isEmpty() || durationEntries.contains(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cucumber.formatter.usage;

import java.util.List;

/**
* Calculate a statistical value to be displayed in the usage-file
*/
public interface UsageStatisticStrategy
{
/**
* @param durationEntries list of execution times of steps as nanoseconds
* @return a statistical value (e.g. median, average, ..)
*/
Long calculate(List<Long> durationEntries);
}
Loading