Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Chris/Patrick - First steps towards fixing the story reporter

  • Loading branch information...
commit b5027ca1b42fdda7c6eabb1fb9d10f0f6bfc5eed 1 parent de45e77
Christopher Kozak authored
Showing with 842 additions and 29 deletions.
  1. +246 −0 src/main/java/com/thoughtworks/core/CustomPostStoryStatisticsCollector.java
  2. +60 −25 src/main/java/com/thoughtworks/core/GlobalStories.java
  3. +29 −0 src/main/java/com/thoughtworks/core/utils/CustomToStringStyle.java
  4. +15 −0 src/main/java/com/thoughtworks/core/utils/GlobalClipboard.java
  5. +66 −0 src/main/java/com/thoughtworks/core/utils/ReflectionTargetedToStringBuilder.java
  6. +55 −0 src/main/java/com/thoughtworks/core/utils/SessionDumpAssertionListener.java
  7. +55 −0 src/main/java/com/thoughtworks/core/utils/xref/CustomCrossReference.java
  8. +39 −0 src/main/java/com/thoughtworks/core/utils/xref/CustomXRefRoot.java
  9. +59 −0 src/main/java/com/thoughtworks/core/utils/xref/CustomXRefStory.java
  10. +13 −0 src/main/java/com/thoughtworks/core/utils/xref/ScenarioResult.java
  11. +38 −0 src/main/java/com/thoughtworks/core/utils/xref/StepOccurrence.java
  12. +135 −0 src/main/java/com/thoughtworks/core/utils/xref/XRefStoryReporter.java
  13. +29 −0 src/main/java/com/thoughtworks/core/utils/xref/XrefStep.java
  14. +1 −1  src/main/resources/stories/google_should_find_me.story
  15. +2 −2 src/main/resources/storynavigator/navigator.html
  16. +0 −1  src/test/java/com/thoughtworks/core/web/VerifyCoreWebIsWorkingTest.java
View
246 src/main/java/com/thoughtworks/core/CustomPostStoryStatisticsCollector.java
@@ -0,0 +1,246 @@
+package com.thoughtworks.core;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.jbehave.core.model.*;
+import org.jbehave.core.reporters.FilePrintStreamFactory;
+import org.jbehave.core.reporters.Format;
+import org.jbehave.core.reporters.StoryReporter;
+import org.jbehave.core.reporters.StoryReporterBuilder;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static java.lang.System.currentTimeMillis;
+import static java.util.Arrays.asList;
+
+public class CustomPostStoryStatisticsCollector implements StoryReporter {
+
+ protected static final String KNOWN_ISSUE = "KnownIssue";
+
+ private final OutputStream output;
+ private final Map<String, Integer> data = new HashMap<String, Integer>();
+ private final List<String> events = asList("notAllowed", "pending", "scenariosNotAllowed",
+ "givenStoryScenariosNotAllowed", "steps", "stepsSuccessful", "stepsIgnorable", "stepsPending",
+ "stepsNotPerformed", "stepsFailed", "currentScenarioSteps", "currentScenarioStepsPending", "scenarios",
+ "scenariosSuccessful", "scenariosPending", "scenariosFailed", "givenStories", "givenStoryScenarios",
+ "givenStoryScenariosSuccessful", "givenStoryScenariosPending", "givenStoryScenariosFailed", "examples", "knownIssue");
+
+ private Throwable cause;
+ private OutcomesTable outcomesFailed;
+ private int givenStories;
+ private long storyStartTime;
+
+ public static Format customStats() {
+ return new Format("STATS") {
+ @Override
+ public StoryReporter createStoryReporter(FilePrintStreamFactory factory, StoryReporterBuilder storyReporterBuilder) {
+ factory.useConfiguration(storyReporterBuilder.fileConfiguration("stats"));
+ return new CustomPostStoryStatisticsCollector(factory.createPrintStream());
+ }
+ };
+ }
+
+ public CustomPostStoryStatisticsCollector(OutputStream output) {
+ this.output = output;
+ }
+
+ public void successful(String step) {
+ add("steps");
+ add("stepsSuccessful");
+ add("currentScenarioSteps");
+ }
+
+ public void ignorable(String step) {
+ add("steps");
+ add("stepsIgnorable");
+ add("currentScenarioSteps");
+ }
+
+ public void pending(String step) {
+ add("steps");
+ add("stepsPending");
+ add("currentScenarioSteps");
+ add("currentScenarioStepsPending");
+ }
+
+ public void notPerformed(String step) {
+ add("steps");
+ add("stepsNotPerformed");
+ add("currentScenarioSteps");
+ }
+
+ public void failed(String step, Throwable cause) {
+ this.cause = cause;
+ add("steps");
+ add("stepsFailed");
+ add("currentScenarioSteps");
+ }
+
+ public void failedOutcomes(String step, OutcomesTable table) {
+ this.outcomesFailed = table;
+ add("steps");
+ add("stepsFailed");
+ add("currentScenarioSteps");
+ }
+
+ public void beforeStory(Story story, boolean givenStory) {
+ if (givenStory) {
+ this.givenStories++;
+ }
+
+ if (!givenStory) {
+ resetData();
+ storyStartTime = currentTimeMillis();
+ }
+
+ if (knownIssue(story.getMeta())) {
+ add("knownIssue");
+ }
+ }
+
+ private boolean knownIssue(Meta meta) {
+ return meta.hasProperty(KNOWN_ISSUE);
+ }
+
+ public void narrative(final Narrative narrative) {
+ }
+
+ public void storyNotAllowed(Story story, String filter) {
+ resetData();
+ add("notAllowed");
+ writeData();
+ }
+
+ public void afterStory(boolean givenStory) {
+ if (givenStory) {
+ this.givenStories--;
+ } else {
+ if (has("scenariosPending") || has("givenStoryScenariosPending")) {
+ add("pending");
+ }
+ int duration = (int) (currentTimeMillis() - storyStartTime);
+ data.put("duration", duration);
+ writeData();
+ }
+ }
+
+ public void givenStories(GivenStories givenStories) {
+ add("givenStories");
+ }
+
+ public void givenStories(List<String> storyPaths) {
+ add("givenStories");
+ }
+
+ public void beforeScenario(String title) {
+ cause = null;
+ outcomesFailed = null;
+ reset("currentScenarioSteps");
+ reset("currentScenarioStepsPending");
+ }
+
+ public void scenarioNotAllowed(Scenario scenario, String filter) {
+ if (givenStories > 0) {
+ add("givenStoryScenariosNotAllowed");
+ } else {
+ add("scenariosNotAllowed");
+ }
+ }
+
+ public void scenarioMeta(Meta meta) {
+ }
+
+ public void afterScenario() {
+ if (givenStories > 0) {
+ countScenarios("givenStoryScenarios");
+ } else {
+ countScenarios("scenarios");
+ }
+ if (has("currentScenarioStepsPending") || !has("currentScenarioSteps")) {
+ if (givenStories > 0) {
+ add("givenStoryScenariosPending");
+ } else {
+ add("scenariosPending");
+ }
+ }
+ }
+
+ private void countScenarios(String namespace) {
+ add(namespace);
+ if (cause != null || outcomesFailed != null) {
+ add(namespace + "Failed");
+ } else {
+ add(namespace + "Successful");
+ }
+ }
+
+ public void beforeExamples(List<String> steps, ExamplesTable table) {
+ }
+
+ public void example(Map<String, String> tableRow) {
+ add("examples");
+ }
+
+ public void afterExamples() {
+ }
+
+ public void dryRun() {
+ }
+
+ public void pendingMethods(List<String> methods) {
+ }
+
+ private void add(String event) {
+ Integer count = data.get(event);
+ if (count == null) {
+ count = 0;
+ }
+ count++;
+ data.put(event, count);
+ }
+
+ private boolean has(String event) {
+ Integer count = data.get(event);
+ if (count == null) {
+ count = 0;
+ }
+ return count > 0;
+ }
+
+ private void writeData() {
+ Properties p = new Properties();
+ for (String event : data.keySet()) {
+ if (!event.startsWith("current")) {
+ p.setProperty(event, data.get(event).toString());
+ }
+ }
+ try {
+ p.store(output, this.getClass().getName());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void resetData() {
+ data.clear();
+ for (String event : events) {
+ reset(event);
+ }
+ }
+
+ private void reset(String event) {
+ data.put(event, 0);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append(output).append(data).toString();
+ }
+
+}
+
View
85 src/main/java/com/thoughtworks/core/GlobalStories.java
@@ -1,8 +1,10 @@
package com.thoughtworks.core;
import com.thoughtworks.core.steps.StepContainer;
-import com.thoughtworks.core.utils.AssertionLog;
-import com.thoughtworks.core.utils.StackTraceAssertionListener;
+import com.thoughtworks.core.utils.*;
+import com.thoughtworks.core.utils.xref.CustomCrossReference;
+import com.thoughtworks.core.utils.xref.XRefStoryReporter;
+import com.thoughtworks.core.web.Browser;
import org.jbehave.core.Embeddable;
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
@@ -12,10 +14,11 @@
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.io.StoryFinder;
import org.jbehave.core.junit.JUnitStories;
-import org.jbehave.core.reporters.CustomHtmlFormat;
-import org.jbehave.core.reporters.Format;
-import org.jbehave.core.reporters.StoryReporterBuilder;
+import org.jbehave.core.reporters.*;
import org.jbehave.core.steps.*;
+import org.jbehave.web.selenium.ContextView;
+import org.jbehave.web.selenium.SeleniumContext;
+import org.jbehave.web.selenium.SeleniumStepMonitor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
@@ -24,6 +27,7 @@
import java.text.SimpleDateFormat;
import java.util.*;
+import static com.thoughtworks.core.CustomPostStoryStatisticsCollector.customStats;
import static java.lang.Boolean.getBoolean;
import static java.util.Arrays.asList;
@@ -32,6 +36,11 @@
private ApplicationContext applicationContext;
@Autowired
protected AssertionLog assertionLog;
+ @Autowired
+ private GlobalClipboard globalClipboard;
+ @Autowired
+ private Browser browser;
+
private List<String> storyFinders;
@@ -40,19 +49,17 @@
public static final String STORIES_DIR = "stories.dir";
private static final Date NOW = new Date();
+ private String metaFilters;
- public GlobalStories(List<String> storyFinders, String metafilters) {
- this(storyFinders, metafilters, new CoreStoryRunner());
- }
-
- public GlobalStories(List<String> storyFinders, String metafilters, StoryRunner storyRunner) {
+ public GlobalStories(List<String> storyFinders, String metaFilters) {
super();
this.storyFinders = storyFinders;
- Embedder embedder = new Embedder(new StoryMapper(), storyRunner, new CustomEmbedderMonitor());
+ Embedder embedder = new Embedder(new StoryMapper(), new CoreStoryRunner(), new CustomEmbedderMonitor());
embedder.useEmbedderControls(new EmbedderControls().doIgnoreFailureInStories(true));
- embedder.useMetaFilters(asList("-Ignore * -DataMissing * " + metafilters));
+ embedder.useMetaFilters(asList("-Ignore * -DataMissing * " + metaFilters));
useEmbedder(embedder);
+ this.metaFilters = metaFilters;
}
public GlobalStories(List<String> storyFinders) {
@@ -69,13 +76,13 @@ public Configuration configuration() {
properties.setProperty("reports", "reports/results-table.ftl");
properties.setProperty("views", "reports/index-view.ftl");
StoryReporterBuilder reporterBuilder = new StoryReporterBuilder()
- .withCodeLocation(CodeLocations.codeLocationFromClass(embeddableClass))
- .withDefaultFormats()
- .withFailureTrace(true)
- .withViewResources(properties)
- .withRelativeDirectory("jbehave" + directorySuffix())
- // withFormats processes the formats based on Alphabetical order and not on who is passed first. TODO create afterStep to improve it
- .withFormats(Format.CONSOLE, Format.XML, CustomHtmlFormat.customHtml(assertionLog));
+ .withCodeLocation(CodeLocations.codeLocationFromClass(embeddableClass))
+ .withDefaultFormats()
+ .withFailureTrace(true)
+ .withViewResources(properties)
+ .withRelativeDirectory("jbehave" + directorySuffix())
+ // withFormats processes the formats based on Alphabetical order and not on who is passed first. TODO create afterStep to improve it
+ .withFormats(Format.CONSOLE, Format.XML, CustomHtmlFormat.customHtml(assertionLog));
System.setProperty(JBEHAVE_REPORT_DIR, reporterBuilder.outputDirectory().toString());
String storiesPath = null;
@@ -86,17 +93,30 @@ public Configuration configuration() {
}
System.setProperty(STORIES_DIR, storiesPath);
+ assertionLog.addAssertionListeners(new SessionDumpAssertionListener(globalClipboard, browser), new HtmlSourceAssertionListener(browser), new ScreenshotAssertionListener(browser));
Configuration configuration = new MostUsefulConfiguration()
- .useParameterConverters(new ParameterConverters().addConverters(new ParameterConverters.StringListConverter("~")))
- .useFailureStrategy(new FailingUponPendingStep())
- .useStoryLoader(new LoadFromClasspath(embeddableClass))
- .useStoryControls(new StoryControls().doSkipScenariosAfterFailure(false))
- .useStoryReporterBuilder(reporterBuilder)
- .useStepFinder(new StepFinder(new ReversedPrioritisingStrategy()));
+ .useParameterConverters(new ParameterConverters().addConverters(new ParameterConverters.StringListConverter("~")))
+ .useFailureStrategy(new FailingUponPendingStep())
+ .useStoryLoader(new LoadFromClasspath(embeddableClass))
+ .useStoryControls(new StoryControls().doSkipScenariosAfterFailure(false))
+ .useStoryReporterBuilder(reporterBuilder)
+ .useStepFinder(new StepFinder(new ReversedPrioritisingStrategy()));
+
+ XRefStoryReporter xRefStoryReporter = new XRefStoryReporter(assertionLog);
+ CustomCrossReference customCrossReference = (CustomCrossReference) new CustomCrossReference(assertionLog, xRefStoryReporter).withMetaFilters(metaFilters).withJsonOnly().withOutputAfterEachStory(true).excludingStoriesWithNoExecutedScenarios(false);
+
+ configuration.storyReporterBuilder().formats().remove(org.jbehave.core.reporters.Format.STATS);
+ configuration.storyReporterBuilder().withFormats(customStats(), new CustomCrossReferenceFormat(xRefStoryReporter));
+ configuration.storyReporterBuilder().withCrossReference(customCrossReference);
+ configuration.useStepMonitor(createStepMonitor(customCrossReference));
return configuration;
}
+ protected StepMonitor createStepMonitor(CustomCrossReference customCrossReference) {
+ return new SeleniumStepMonitor(new ContextView.NULL(), new SeleniumContext(), customCrossReference.getStepMonitor());
+ }
+
private String directorySuffix() {
String datePattern = "MM-dd-yy_H-mm-ss";
SimpleDateFormat dateFormatter = new SimpleDateFormat(datePattern);
@@ -120,6 +140,21 @@ public StepContainer getSteps() {
return applicationContext.getBean("stepContainer", StepContainer.class);
}
+ private class CustomCrossReferenceFormat extends Format {
+ private XRefStoryReporter reporter;
+
+ public CustomCrossReferenceFormat(XRefStoryReporter reporter) {
+ super("XREF");
+ this.reporter = reporter;
+ }
+
+ @Override
+ public StoryReporter createStoryReporter(FilePrintStreamFactory factory, StoryReporterBuilder storyReporterBuilder) {
+ return reporter;
+ }
+ }
+
+
private class ReversedPrioritisingStrategy implements StepFinder.PrioritisingStrategy {
public List<StepCandidate> prioritise(String stepAsString, List<StepCandidate> candidates) {
Collections.sort(candidates, new Comparator<StepCandidate>() {
View
29 src/main/java/com/thoughtworks/core/utils/CustomToStringStyle.java
@@ -0,0 +1,29 @@
+package com.thoughtworks.core.utils;
+
+import org.apache.commons.lang.builder.ToStringStyle;
+
+public class CustomToStringStyle {
+
+ public static final ToStringStyle MINIMAL_STYLE = new MinimalToStringStyle();
+ public static final ToStringStyle SHORT_PREFIX_MULTI_LINE_STYLE = new ShortPrefixMultiLineToStringStyle();
+
+ private static final class MinimalToStringStyle extends ToStringStyle {
+ MinimalToStringStyle() {
+ super();
+ this.setUseShortClassName(true);
+ this.setUseIdentityHashCode(false);
+ this.setUseFieldNames(false);
+ }
+ }
+
+ private static final class ShortPrefixMultiLineToStringStyle extends ToStringStyle {
+ ShortPrefixMultiLineToStringStyle() {
+ super();
+ this.setUseShortClassName(true);
+ this.setUseIdentityHashCode(false);
+ this.setFieldSeparatorAtStart(true);
+ this.setFieldSeparator("\n ");
+ this.setContentEnd("\n]");
+ }
+ }
+}
View
15 src/main/java/com/thoughtworks/core/utils/GlobalClipboard.java
@@ -0,0 +1,15 @@
+package com.thoughtworks.core.utils;
+
+import org.springframework.stereotype.Component;
+
+import static com.thoughtworks.core.utils.CustomToStringStyle.SHORT_PREFIX_MULTI_LINE_STYLE;
+import static com.thoughtworks.core.utils.ReflectionTargetedToStringBuilder.reflectionTargetedToString;
+
+@Component
+public class GlobalClipboard {
+ @Override
+ public String toString() {
+ return reflectionTargetedToString(this, SHORT_PREFIX_MULTI_LINE_STYLE);
+ }
+
+}
View
66 src/main/java/com/thoughtworks/core/utils/ReflectionTargetedToStringBuilder.java
@@ -0,0 +1,66 @@
+package com.thoughtworks.core.utils;
+
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+
+import java.lang.reflect.Field;
+import java.util.Collection;
+import java.util.Map;
+
+import static org.apache.commons.lang.StringUtils.isBlank;
+import static org.apache.commons.lang.builder.ToStringStyle.SHORT_PREFIX_STYLE;
+
+public class ReflectionTargetedToStringBuilder extends ReflectionToStringBuilder {
+
+ public ReflectionTargetedToStringBuilder(Object object, ToStringStyle style) {
+ super(object, style);
+ }
+
+ public static String reflectionTargetedToString(Object object) {
+ return reflectionTargetedToString(object, SHORT_PREFIX_STYLE);
+ }
+
+ public static String reflectionTargetedToString(Object object, ToStringStyle style) {
+ return new ReflectionTargetedToStringBuilder(object, style).toString();
+ }
+
+ @Override
+ protected boolean accept(Field field) {
+ return (super.accept(field) && getValueForField(field) != null && !isEmptyCollection(field) && !isEmptyMap(field) && !isBlankString(field));
+ }
+
+ private boolean isBlankString(Field field) {
+ if (field.getType().equals(String.class)) {
+ String string = (String) getValueForField(field);
+ return isBlank(string);
+ }
+
+ return false;
+ }
+
+ private boolean isEmptyMap(Field field) {
+ if (Map.class.isAssignableFrom(field.getType())) {
+ Map map = (Map) getValueForField(field);
+ return map.isEmpty();
+ }
+
+ return false;
+ }
+
+ private boolean isEmptyCollection(Field field) {
+ if (Collection.class.isAssignableFrom(field.getType())) {
+ Collection collection = (Collection) getValueForField(field);
+ return collection.isEmpty();
+ }
+
+ return false;
+ }
+
+ private Object getValueForField(Field field) {
+ Object value = null;
+ try {
+ value = super.getValue(field);
+ } catch (IllegalAccessException e) { }
+ return value;
+ }
+}
View
55 src/main/java/com/thoughtworks/core/utils/SessionDumpAssertionListener.java
@@ -0,0 +1,55 @@
+package com.thoughtworks.core.utils;
+
+import com.thoughtworks.core.web.Browser;
+import org.apache.log4j.Logger;
+
+import java.io.File;
+
+import static com.thoughtworks.core.utils.CoreUtils.getLoggerFor;
+import static org.apache.commons.io.FileUtils.writeStringToFile;
+
+public class SessionDumpAssertionListener extends AssertionListener {
+ private Logger logger = getLoggerFor(this);
+ private GlobalClipboard globalClipboard;
+ private Browser browser;
+
+ public SessionDumpAssertionListener(GlobalClipboard globalClipboard, Browser browser) {
+ this.globalClipboard = globalClipboard;
+ this.browser = browser;
+ }
+
+ @Override
+ public void afterAssertFailed() {
+ createSessionDumpFile();
+ }
+
+ @Override
+ public String afterScenario(String storyName) {
+ createSessionDumpFile();
+ return createLinkToSessionDump(storyName);
+ }
+
+ private void createSessionDumpFile() {
+ try {
+ File file = getUniqueFile(resetUUID(), "txt");
+ logger.info("Session Dump file: " + file.getAbsolutePath());
+ writeStringToFile(file, globalClipboard.toString());
+ } catch (Exception e) {
+ logger.error("Session information failed to write to disk! ", e);
+ }
+ }
+
+ @Override
+ public String afterStepFailed(AssertionLog assertionLog, String storyName, String step, Throwable failure) {
+ if (!getUniqueFile(resetUUID(), "txt").exists()){
+ afterAssertFailed();
+ }
+
+ return createLinkToSessionDump(storyName);
+ }
+
+ private String createLinkToSessionDump(String storyName) {
+ String href = "../resources/" + storyName + currentUUID() + ".txt";
+ return "<div><a href='" + href + "'>Session Dump</a></div>";
+ }
+}
View
55 src/main/java/com/thoughtworks/core/utils/xref/CustomCrossReference.java
@@ -0,0 +1,55 @@
+package com.thoughtworks.core.utils.xref;
+
+import com.thoughtworks.core.utils.AssertionLog;
+import com.thoughtworks.xstream.XStream;
+import org.jbehave.core.model.Scenario;
+import org.jbehave.core.reporters.CrossReference;
+
+public class CustomCrossReference extends CrossReference {
+
+ private String metaFilters;
+ private AssertionLog assertionLog;
+ private XRefStoryReporter xrefStoryReporter;
+
+ public CustomCrossReference(AssertionLog assertionLog, XRefStoryReporter xrefStoryReporter) {
+ this.assertionLog = assertionLog;
+ this.xrefStoryReporter = xrefStoryReporter;
+ }
+
+ protected void aliasForXRefStory(XStream xstream) {
+ xstream.alias("story", CustomXRefStory.class);
+ }
+
+ protected void aliasForXRefRoot(XStream xstream) {
+ xstream.alias("xref", CustomXRefRoot.class);
+ ignoreStepMatchesFromXrefRootNowThatWeAreUsingACustomXrefRoot(xstream);
+ ignoreUnwantedFieldOnScenarioClass(xstream);
+ }
+
+ private void ignoreUnwantedFieldOnScenarioClass(XStream xstream) {
+ xstream.omitField(Scenario.class, "examplesTable");
+ xstream.omitField(Scenario.class, "givenStories");
+ xstream.omitField(Scenario.class, "meta");
+ xstream.omitField(XRefStory.class, "name");
+ }
+
+ private void ignoreStepMatchesFromXrefRootNowThatWeAreUsingACustomXrefRoot(XStream xstream) {
+ xstream.omitField(XRefRoot.class, "stepMatches");
+ }
+
+ @Override
+ protected XRefRoot newXRefRoot() {
+ return new CustomXRefRoot(assertionLog, xrefStoryReporter.getScenarioResults(), xrefStoryReporter.getXrefSteps());
+ }
+
+ public CrossReference withMetaFilters(String metaFilters) {
+ this.metaFilters = metaFilters;
+ return this;
+ }
+
+ @Override
+ public String getMetaFilter() {
+ return metaFilters;
+ }
+
+}
View
39 src/main/java/com/thoughtworks/core/utils/xref/CustomXRefRoot.java
@@ -0,0 +1,39 @@
+package com.thoughtworks.core.utils.xref;
+
+import com.thoughtworks.core.utils.AssertionLog;
+import org.jbehave.core.model.Story;
+import org.jbehave.core.reporters.CrossReference;
+import org.jbehave.core.reporters.StoryReporterBuilder;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+// Understands how to represent the data source to be used by the story navigator
+public class CustomXRefRoot extends CrossReference.XRefRoot {
+ private Set<String> metaTags = new HashSet<String>();
+ private List<XrefStep> stepOccurrences;
+
+ private transient AssertionLog assertionLog;
+ private transient Map<Story, List<ScenarioResult>> scenarioResults;
+
+ public CustomXRefRoot(AssertionLog assertionLog, Map<Story, List<ScenarioResult>> scenarioResults, List<XrefStep> xrefSteps) {
+ this.assertionLog = assertionLog;
+ this.scenarioResults = scenarioResults;
+ this.stepOccurrences = xrefSteps;
+ }
+
+ @Override
+ protected String createdBy() {
+ return "FTA";
+ }
+
+ protected CrossReference.XRefStory createXRefStory(StoryReporterBuilder storyReporterBuilder, Story story, boolean passed) {
+ return new CustomXRefStory(story, storyReporterBuilder, passed, scenarioResults.get(story));
+ }
+
+ public void addMetaOption(String option) {
+ metaTags.add(option);
+ }
+}
View
59 src/main/java/com/thoughtworks/core/utils/xref/CustomXRefStory.java
@@ -0,0 +1,59 @@
+package com.thoughtworks.core.utils.xref;
+
+import org.jbehave.core.model.Meta;
+import org.jbehave.core.model.Story;
+import org.jbehave.core.reporters.CrossReference;
+import org.jbehave.core.reporters.StoryReporterBuilder;
+
+import java.util.List;
+
+import static java.lang.Boolean.FALSE;
+import static org.apache.commons.lang.StringUtils.*;
+
+// Understands how to represent the story in the data source used by the story navigator
+public class CustomXRefStory extends CrossReference.XRefStory {
+ protected static final String KNOWN_ISSUE = "KnownIssue";
+
+ transient private Story story;
+ private String knownIssue = "";
+ private Boolean hasKnownIssue = FALSE;
+ private List<ScenarioResult> scenarioResults;
+ private String readableName = null;
+
+ public CustomXRefStory(Story story, StoryReporterBuilder storyReporterBuilder, boolean passed, List<ScenarioResult> scenarioResults) {
+ super(story, storyReporterBuilder, passed);
+ this.story = story;
+ this.scenarioResults = scenarioResults;
+ this.readableName = convertToHumanReadableName(story.getName());
+
+ if (knownIssue(story.getMeta())) {
+ hasKnownIssue = true;
+ knownIssue = story.getMeta().getProperty(KNOWN_ISSUE);
+ }
+ }
+
+
+ private String convertToHumanReadableName(String name) {
+ return capitalize(name.replaceAll(".story", "").replaceAll("_", " "));
+ }
+
+ private boolean knownIssue(Meta meta) {
+ return meta.hasProperty(KNOWN_ISSUE);
+ }
+
+ @Override
+ protected void processMetaTags(CrossReference.XRefRoot root) {
+ // This is needed to make sure that the meta property gets set for each story.
+ // We ignore the changes to the meta field in the XRefRoot class.
+ super.processMetaTags(root);
+
+ populateRootMetaTags((CustomXRefRoot) root);
+ }
+
+ private void populateRootMetaTags(CustomXRefRoot root) {
+ for (String next : story.getMeta().getPropertyNames()) {
+ root.addMetaOption(next + "=" + story.getMeta().getProperty(next));
+ }
+ }
+
+}
View
13 src/main/java/com/thoughtworks/core/utils/xref/ScenarioResult.java
@@ -0,0 +1,13 @@
+package com.thoughtworks.core.utils.xref;
+
+class ScenarioResult {
+ private String title;
+ private String failure;
+ private Boolean passed;
+
+ ScenarioResult(String title, String failure, Boolean passed) {
+ this.title = title;
+ this.failure = failure;
+ this.passed = passed;
+ }
+}
View
38 src/main/java/com/thoughtworks/core/utils/xref/StepOccurrence.java
@@ -0,0 +1,38 @@
+package com.thoughtworks.core.utils.xref;
+
+import org.jbehave.core.model.Meta;
+import org.jbehave.core.model.Scenario;
+import org.jbehave.core.model.Story;
+
+import static com.thoughtworks.core.utils.xref.CustomXRefStory.KNOWN_ISSUE;
+import static org.apache.commons.lang.StringUtils.isBlank;
+import static org.apache.commons.lang.StringUtils.substringBefore;
+
+class StepOccurrence {
+ private String story;
+ private String scenario;
+ private String step;
+ private Outcome outcome;
+ private Boolean isKnownIssue = false;
+
+ public StepOccurrence(Story story, Scenario scenario, String step, Outcome outcome) {
+ this.story = story.getPath();
+ this.scenario = scenario.getTitle();
+ this.step = step;
+ this.outcome = outcome;
+ this.isKnownIssue = knownIssue(story.getMeta());
+ }
+
+ private boolean knownIssue(Meta meta) {
+ return meta.hasProperty(KNOWN_ISSUE);
+ }
+
+ private boolean knownIssueForAllEnvironments(String propertyValue) {
+ return isBlank(substringBefore(propertyValue, "-"));
+ }
+
+ public static enum Outcome {
+ NOT_PERFORMED, PASSED, PENDING, FAILED, IGNORED;
+
+ }
+}
View
135 src/main/java/com/thoughtworks/core/utils/xref/XRefStoryReporter.java
@@ -0,0 +1,135 @@
+package com.thoughtworks.core.utils.xref;
+
+import com.thoughtworks.core.steps.ScenarioFailedFromAssertionFailuresException;
+import com.thoughtworks.core.utils.AssertionLog;
+import org.jbehave.core.failures.UUIDExceptionWrapper;
+import org.jbehave.core.model.Scenario;
+import org.jbehave.core.model.Story;
+import org.jbehave.core.reporters.NullStoryReporter;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static ch.lambdaj.Lambda.*;
+import static org.hamcrest.CoreMatchers.is;
+
+public class XRefStoryReporter extends NullStoryReporter {
+ private Map<Story, List<ScenarioResult>> scenarioResults = new HashMap<Story, List<ScenarioResult>>();
+ private List<XrefStep> xrefSteps = new ArrayList<XrefStep>();
+ private Story currentStory;
+ private Scenario currentScenario;
+ private Throwable currentFailure;
+ private AssertionLog assertionLog;
+ private Integer assertionFailureCount = 0;
+ private static final String STRANGE_OPEN_PARENS = "";
+ private static final String STRANGE_CLOSE_PARENS = "";
+ private String lastStepType;
+
+ public XRefStoryReporter(AssertionLog assertionLog) {
+ this.assertionLog = assertionLog;
+ }
+
+ @Override
+ public void beforeStory(Story story, boolean givenStory) {
+ currentStory = story;
+ scenarioResults.put(currentStory, new ArrayList<ScenarioResult>());
+ }
+
+ @Override
+ public void beforeScenario(String scenarioTitle) {
+ currentScenario = selectFirst(currentStory.getScenarios(), having(on(Scenario.class).getTitle(), is(scenarioTitle)));
+ currentFailure = null;
+ assertionFailureCount = 0;
+ }
+
+ @Override
+ public void successful(String step) {
+ if (assertionLog.failureCount() > assertionFailureCount) {
+ handleAssertFailure(step);
+ return;
+ }
+ addOccurrenceFor(step, StepOccurrence.Outcome.PASSED);
+ }
+
+ private void handleAssertFailure(String step) {
+ failed(step, new UUIDExceptionWrapper(assertionLog.getLastAssertionFailure().getFailure()));
+ assertionFailureCount++;
+ }
+
+ @Override
+ public void ignorable(String step) {
+ addOccurrenceFor(step, StepOccurrence.Outcome.IGNORED);
+ }
+
+ @Override
+ public void pending(String step) {
+ addOccurrenceFor(step, StepOccurrence.Outcome.PENDING);
+ }
+
+ @Override
+ public void notPerformed(String step) {
+ addOccurrenceFor(step, StepOccurrence.Outcome.NOT_PERFORMED);
+ }
+
+ @Override
+ public void failed(String step, Throwable cause) {
+ if (cause.getCause().getCause() instanceof ScenarioFailedFromAssertionFailuresException) {
+ return;
+ }
+
+ addOccurrenceFor(step, StepOccurrence.Outcome.FAILED);
+ currentFailure = cause;
+ }
+
+ @Override
+ public void afterScenario() {
+ scenarioResults.get(currentStory).add(new ScenarioResult(currentScenario.getTitle(), extractFailureMessage(), currentFailure == null));
+ }
+
+ private String extractFailureMessage() {
+ if (currentFailure != null) {
+ if (currentFailure.getCause() != null) {
+ return currentFailure.getCause().getMessage();
+ }
+ return currentFailure.getMessage();
+ }
+ return null;
+ }
+
+ private void addOccurrenceFor(String step, StepOccurrence.Outcome outcome) {
+ xrefFor(step).addOccurrence(new StepOccurrence(currentStory, currentScenario, step, outcome));
+ }
+
+ private XrefStep xrefFor(String stepString) {
+ String[] tokens = stepString.split(" ");
+ String stepType = tokens[0].toUpperCase();
+ String stepText = stepString.substring(stepString.indexOf(" ") + 1);
+ stepText = stepText.replaceAll(STRANGE_OPEN_PARENS + ".+" + STRANGE_CLOSE_PARENS, "X");
+ if (stepType.equalsIgnoreCase("and")) {
+ stepType = lastStepType;
+ } else {
+ lastStepType = stepType;
+ }
+ XrefStep xrefStep = new XrefStep(lastStepType, stepText);
+ return getOrCreate(xrefStep);
+ }
+
+ private XrefStep getOrCreate(XrefStep newXrefStep) {
+ for (XrefStep xrefStep : xrefSteps) {
+ if (xrefStep.getType().equals(newXrefStep.getType()) && xrefStep.getAnnotatedPattern().equals(newXrefStep.getAnnotatedPattern()))
+ return xrefStep;
+ }
+ xrefSteps.add(newXrefStep);
+ return newXrefStep;
+ }
+
+ public List<XrefStep> getXrefSteps() {
+ return xrefSteps;
+ }
+
+ public Map<Story, List<ScenarioResult>> getScenarioResults() {
+ return scenarioResults;
+ }
+}
View
29 src/main/java/com/thoughtworks/core/utils/xref/XrefStep.java
@@ -0,0 +1,29 @@
+package com.thoughtworks.core.utils.xref;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class XrefStep {
+
+ private String type;
+ private String annotatedPattern; // TODO: MATT - this is not the annotated pattern (not really -> we should use a XrefStepMonitor to get the real annotated pattern)
+ private List<StepOccurrence> occurrences = new ArrayList<StepOccurrence>();
+
+ public XrefStep(String stepType, String annotatedPattern) {
+ this.type = stepType;
+ this.annotatedPattern = annotatedPattern;
+ }
+
+ public void addOccurrence(StepOccurrence occurrence) {
+ occurrences.add(occurrence);
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getAnnotatedPattern() {
+ return annotatedPattern;
+ }
+
+}
View
2  src/main/resources/stories/google_should_find_me.story
@@ -2,5 +2,5 @@
Scenario: Google should search find my profile
Given I am visiting the Googles
-When I search for 'Chris Kozak'
+When I search for 'Christobal Kozak'
Then I should see my profile
View
4 src/main/resources/storynavigator/navigator.html
@@ -13,7 +13,7 @@
<script type="text/javascript" src="js/jquery-framedialog-1.1.2.js"></script>
<link rel="stylesheet" type="text/css" href="style/navigator.css"/>
<link rel="stylesheet" type="text/css" href="style/jquery-ui.css"/>
- <title>FTA Report</title>
+ <title>Story Navigator</title>
</head>
<body ng:controller="MyController">
@@ -29,7 +29,7 @@
<div id="inside">
<div id="top-bar">
<img src="images/logo.svg.png" width="60px" />
- <h1 id="title">FTA Report</h1>
+ <h1 id="title">Story Navigator</h1>
<div class="timestamp">Generated by {{data.xref.createdBy}} on {{ data.xref.whenMade | date:'yyyy-MM-dd @ HH:mm'}}
</div>
View
1  src/test/java/com/thoughtworks/core/web/VerifyCoreWebIsWorkingTest.java
@@ -1,7 +1,6 @@
package com.thoughtworks.core.web;
import com.thoughtworks.core.GlobalStories;
-import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
Please sign in to comment.
Something went wrong with that request. Please try again.