Skip to content

Commit

Permalink
Issue cucumber#207 UsageFormatter
Browse files Browse the repository at this point in the history
  • Loading branch information
klausbayrhammer committed Feb 19, 2012
1 parent c370c6d commit 9c93be9
Show file tree
Hide file tree
Showing 9 changed files with 567 additions and 1 deletion.
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;
}
}
217 changes: 217 additions & 0 deletions core/src/main/java/cucumber/formatter/UsageFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
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.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()
{
for (Map.Entry<String, List<Long>> usageEntry : usageMap.entrySet())
{
printAggregatedResult(usageEntry);

String key = usageEntry.getKey();
for (Long duration : usageEntry.getValue())
{
printSingleDurationEntry(key, duration);
}
}
}

private void printAggregatedResult(Map.Entry<String, List<Long>> usageEntry)
{
String stepName = usageEntry.getKey();
for (Map.Entry<String, UsageStatisticStrategy> calculatorEntry : statisticStrategies.entrySet())
{
String calculatorName = calculatorEntry.getKey();
UsageStatisticStrategy statisticStrategy = calculatorEntry.getValue();

Long calculationResult = statisticStrategy.calculate(usageEntry.getValue());
out.println(String.format("%s (%s) : %s", formatDuration(calculationResult), calculatorName, stepName));
}
}

private void printSingleDurationEntry(String key, Long duration)
{
String formattedDuration = formatDuration(duration);
out.println("\t" + formattedDuration + " : " + key);
}

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

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

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

private String formatStepNameWithArgs(Result result, Step step)
{
StringBuffer buffer = new StringBuffer();
Format format = getFormat(result.getStatus());
Format argFormat = getArgFormat(result.getStatus());
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);
}
}
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);
}
19 changes: 18 additions & 1 deletion core/src/test/java/cucumber/formatter/FormatterFactoryTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package cucumber.formatter;

import cucumber.formatter.usage.AverageUsageStatisticStrategy;
import cucumber.formatter.usage.MedianUsageStatisticStrategy;
import cucumber.formatter.usage.UsageStatisticStrategy;
import gherkin.formatter.Formatter;
import gherkin.formatter.JSONFormatter;
import gherkin.formatter.JSONPrettyFormatter;
Expand All @@ -8,10 +11,13 @@

import java.io.File;
import java.io.StringWriter;
import java.util.Map;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;

public class FormatterFactoryTest {

Expand Down Expand Up @@ -42,6 +48,18 @@ public void shouldInstantiateHtmlFormatter() {
assertThat(formatterFactory.createFormatter("html", new File(System.getProperty("user.dir"))), is(HTMLFormatter.class));
}


@Test
public void shouldInstantiateUsageFormatter() {
Formatter formatter = formatterFactory.createFormatter("usage", mock(Appendable.class));

assertThat(formatter, is(UsageFormatter.class));
Map<String,UsageStatisticStrategy> statisticStrategies = ((UsageFormatter) formatter).statisticStrategies;
assertEquals(statisticStrategies.size(), 2);
assertThat(statisticStrategies.get("average"), is(AverageUsageStatisticStrategy.class));
assertThat(statisticStrategies.get("median"), is(MedianUsageStatisticStrategy.class));
}

@Test
public void shouldInstantiateCustomFormatterFromClassNameWithAppender() {
StringWriter writer = new StringWriter();
Expand All @@ -57,5 +75,4 @@ public void shouldInstantiateCustomFormatterFromClassNameWithDirFile() {
assertThat(formatter, is(TestFormatter.class));
assertSame(dir, ((TestFormatter) formatter).dir);
}

}
Loading

0 comments on commit 9c93be9

Please sign in to comment.