Coverage Report - org.jbehave.core.reporters.CrossReference
 
Classes in this File Line Coverage Branch Coverage Complexity
CrossReference
91%
71/78
83%
5/6
1.778
CrossReference$1
94%
18/19
50%
1/2
1.778
CrossReference$StepMatch
35%
7/20
0%
0/18
1.778
CrossReference$StepUsage
100%
5/5
N/A
1.778
CrossReference$StoryHolder
100%
5/5
N/A
1.778
CrossReference$XRefRoot
97%
39/40
62%
10/16
1.778
CrossReference$XRefStepMonitor
75%
12/16
50%
3/6
1.778
CrossReference$XRefStory
82%
28/34
60%
6/10
1.778
CrossReference$XrefOutputFailed
0%
0/2
N/A
1.778
 
 1  
 package org.jbehave.core.reporters;
 2  
 
 3  
 import java.io.File;
 4  
 import java.io.FileWriter;
 5  
 import java.io.IOException;
 6  
 import java.io.Writer;
 7  
 import java.lang.reflect.Method;
 8  
 import java.util.ArrayList;
 9  
 import java.util.HashMap;
 10  
 import java.util.HashSet;
 11  
 import java.util.List;
 12  
 import java.util.Map;
 13  
 import java.util.Set;
 14  
 
 15  
 import org.jbehave.core.io.StoryLocation;
 16  
 import org.jbehave.core.model.ExamplesTable;
 17  
 import org.jbehave.core.model.Meta;
 18  
 import org.jbehave.core.model.Narrative;
 19  
 import org.jbehave.core.model.Scenario;
 20  
 import org.jbehave.core.model.StepPattern;
 21  
 import org.jbehave.core.model.Story;
 22  
 import org.jbehave.core.steps.NullStepMonitor;
 23  
 import org.jbehave.core.steps.StepMonitor;
 24  
 import org.jbehave.core.steps.StepType;
 25  
 
 26  
 import com.thoughtworks.xstream.XStream;
 27  
 import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
 28  
 
 29  56
 public class CrossReference extends Format {
 30  
 
 31  5
     private final XStream XSTREAM_FOR_XML = new XStream();
 32  5
     private final XStream XSTREAM_FOR_JSON = new XStream(new JsonHierarchicalStreamDriver());
 33  5
     private ThreadLocal<Story> currentStory = new ThreadLocal<Story>();
 34  5
     private ThreadLocal<Long> currentStoryStart = new ThreadLocal<Long>();
 35  5
     private ThreadLocal<String> currentScenarioTitle = new ThreadLocal<String>();
 36  5
     private List<StoryHolder> stories = new ArrayList<StoryHolder>();
 37  5
     private Map<String, Long> times = new HashMap<String, Long>();
 38  5
     private Map<String, StepMatch> stepMatches = new HashMap<String, StepMatch>();
 39  5
     private StepMonitor stepMonitor = new XRefStepMonitor();
 40  5
     private Set<String> failingStories = new HashSet<String>();
 41  5
     private Set<String> stepsPerformed = new HashSet<String>();
 42  5
     private boolean doJson = true;
 43  5
     private boolean doXml = true;
 44  5
     private boolean excludeStoriesWithNoExecutedScenarios = false;
 45  5
     private boolean outputAfterEachStory = false;
 46  
     private Format threadSafeDelegateFormat;
 47  
 
 48  
     public CrossReference() {
 49  5
         this("XREF");
 50  5
     }
 51  
 
 52  
     public CrossReference(String name) {
 53  5
         super(name);
 54  5
         configure(XSTREAM_FOR_XML);
 55  5
         configure(XSTREAM_FOR_JSON);
 56  5
     }
 57  
 
 58  
     public CrossReference withJsonOnly() {
 59  1
         doJson = true;
 60  1
         doXml = false;
 61  1
         return this;
 62  
     }
 63  
 
 64  
     public CrossReference withXmlOnly() {
 65  1
         doJson = false;
 66  1
         doXml = true;
 67  1
         return this;
 68  
     }
 69  
 
 70  
     public CrossReference withOutputAfterEachStory(boolean outputAfterEachStory) {
 71  0
         this.outputAfterEachStory = outputAfterEachStory;
 72  0
         return this;
 73  
     }
 74  
 
 75  
     public CrossReference withThreadSafeDelegateFormat(Format format) {
 76  0
         this.threadSafeDelegateFormat = format;
 77  0
         return this;
 78  
     }
 79  
 
 80  
     public CrossReference excludingStoriesWithNoExecutedScenarios(boolean exclude) {
 81  4
         this.excludeStoriesWithNoExecutedScenarios = exclude;
 82  4
         return this;
 83  
     }
 84  
 
 85  
     public StepMonitor getStepMonitor() {
 86  4
         return stepMonitor;
 87  
     }
 88  
 
 89  
     /**
 90  
      * Output to JSON and/or XML files.  Could be at the end of the suite, or per story
 91  
      * In the case of the latter, synchronization is needed as two stories (on two threads) could
 92  
      * be completing concurrently, and we need to guard against ConcurrentModificationException
 93  
      * @param storyReporterBuilder the reporter to use
 94  
      */
 95  
     public synchronized void outputToFiles(StoryReporterBuilder storyReporterBuilder) {
 96  5
         XRefRoot root = createXRefRoot(storyReporterBuilder, stories, failingStories);
 97  5
         root.addStepMatches(stepMatches);
 98  5
         if (doXml) {
 99  4
             outputFile(fileName("xml"), XSTREAM_FOR_XML, root, storyReporterBuilder);
 100  
         }
 101  5
         if (doJson) {
 102  4
             outputFile(fileName("json"), XSTREAM_FOR_JSON, root, storyReporterBuilder);
 103  
         }
 104  5
     }
 105  
 
 106  
     protected String fileName(String extension) {
 107  8
         return name().toLowerCase() + "." + extension;
 108  
     }
 109  
 
 110  
     protected final XRefRoot createXRefRoot(StoryReporterBuilder storyReporterBuilder, List<StoryHolder> stories,
 111  
             Set<String> failingStories) {
 112  5
         XRefRoot xrefRoot = newXRefRoot();
 113  5
         xrefRoot.metaFilter = getMetaFilter();
 114  5
         xrefRoot.setExcludeStoriesWithNoExecutedScenarios(excludeStoriesWithNoExecutedScenarios);
 115  5
         xrefRoot.processStories(stories, stepsPerformed,  times, storyReporterBuilder, failingStories);
 116  5
         return xrefRoot;
 117  
     }
 118  
 
 119  
     protected XRefRoot newXRefRoot() {
 120  1
         return new XRefRoot();
 121  
     }
 122  
     
 123  
     private void outputFile(String name, XStream xstream, XRefRoot root, StoryReporterBuilder storyReporterBuilder) {
 124  
 
 125  8
         File outputDir = new File(storyReporterBuilder.outputDirectory(), "view");
 126  8
         outputDir.mkdirs();
 127  
         try {
 128  8
             Writer writer = makeWriter(new File(outputDir, name));
 129  8
             writer.write(xstream.toXML(root));
 130  8
             writer.flush();
 131  8
             writer.close();
 132  0
         } catch (IOException e) {
 133  0
             throw new XrefOutputFailed(name, e);
 134  8
         }
 135  
 
 136  8
     }
 137  
 
 138  
     /** Override this if the metaFilter is important to
 139  
      * you in the Story Navigator output */
 140  
     public String getMetaFilter() {
 141  2
         return "";
 142  
     }
 143  
 
 144  
     @SuppressWarnings("serial")
 145  
     public static class XrefOutputFailed extends RuntimeException {
 146  
 
 147  
         public XrefOutputFailed(String name, Throwable cause) {
 148  0
             super(name, cause);
 149  0
         }
 150  
 
 151  
     }
 152  
 
 153  
     protected Writer makeWriter(File file) throws IOException {
 154  2
         return new FileWriter(file);
 155  
     }
 156  
 
 157  
     private void configure(XStream xstream) {
 158  10
         xstream.setMode(XStream.NO_REFERENCES);
 159  10
         aliasForXRefRoot(xstream);
 160  10
         aliasForXRefStory(xstream);
 161  10
         xstream.alias("stepMatch", StepMatch.class);
 162  10
         xstream.alias("pattern", StepPattern.class);
 163  10
         xstream.alias("use", StepUsage.class);
 164  10
         xstream.omitField(ExamplesTable.class, "parameterConverters");
 165  10
         xstream.omitField(ExamplesTable.class, "defaults");
 166  10
     }
 167  
 
 168  
     protected void aliasForXRefStory(XStream xstream) {
 169  8
         xstream.alias("story", XRefStory.class);
 170  8
     }
 171  
 
 172  
     protected void aliasForXRefRoot(XStream xstream) {
 173  2
         xstream.alias("xref", XRefRoot.class);
 174  2
     }
 175  
 
 176  
     @Override
 177  
     public StoryReporter createStoryReporter(FilePrintStreamFactory factory, final StoryReporterBuilder storyReporterBuilder) {
 178  
         StoryReporter delegate;
 179  4
         if (threadSafeDelegateFormat == null) {
 180  4
             delegate = new NullStoryReporter();
 181  
         } else {
 182  0
             delegate = threadSafeDelegateFormat.createStoryReporter(factory, storyReporterBuilder);
 183  
         }
 184  4
         return new DelegatingStoryReporter(delegate) {
 185  
 
 186  
             @Override
 187  
             public void beforeStory(Story story, boolean givenStory) {
 188  4
                 synchronized (this) {
 189  4
                     stories.add(new StoryHolder(story));
 190  4
                 }
 191  4
                 currentStory.set(story);
 192  4
                 currentStoryStart.set(System.currentTimeMillis());
 193  4
                 super.beforeStory(story, givenStory);
 194  4
             }
 195  
 
 196  
             @Override
 197  
             public void failed(String step, Throwable cause) {
 198  4
                 failingStories.add(currentStory.get().getPath());
 199  4
                 super.failed(step, cause);
 200  4
             }
 201  
 
 202  
             @Override
 203  
             public void afterStory(boolean givenStory) {
 204  4
                 times.put(currentStory.get().getPath(), System.currentTimeMillis() - currentStoryStart.get());
 205  4
                 if (outputAfterEachStory) {
 206  0
                     outputToFiles(storyReporterBuilder);
 207  
                 }
 208  4
                 super.afterStory(givenStory);
 209  4
             }
 210  
 
 211  
             @Override
 212  
             public void beforeScenario(String title) {
 213  4
                 currentScenarioTitle.set(title);
 214  4
                 super.beforeScenario(title);
 215  4
             }
 216  
         };
 217  
     }
 218  
 
 219  10
     private class XRefStepMonitor extends NullStepMonitor {
 220  
         @Override
 221  
         public void performing(String step, boolean dryRun) {
 222  0
             super.performing(step, dryRun);
 223  0
             stepsPerformed.add(currentStory.get().getPath());
 224  0
         }
 225  
 
 226  
         public void stepMatchesPattern(String step, boolean matches, StepPattern pattern, Method method,
 227  
                 Object stepsInstance) {
 228  4
             Story story = currentStory.get();
 229  4
             if (story == null) {
 230  0
                 throw new NullPointerException("story not setup for CrossReference");
 231  
             }
 232  
 
 233  4
             if (matches) {
 234  4
                 String key = pattern.type() + pattern.annotated();
 235  4
                 StepMatch stepMatch = stepMatches.get(key);
 236  4
                 if (stepMatch == null) {
 237  4
                     stepMatch = new StepMatch(pattern.type(), pattern.annotated(), pattern.resolved());
 238  4
                     stepMatches.put(key, stepMatch);
 239  
                 }
 240  
                 // find canonical ref for same stepMatch
 241  4
                 stepMatch.usages.add(new StepUsage(story.getPath(), currentScenarioTitle.get(), step));
 242  
             }
 243  4
             super.stepMatchesPattern(step, matches, pattern, method, stepsInstance);
 244  4
         }
 245  
     }
 246  
 
 247  8
     public static class XRefRoot {
 248  5
         protected long whenMade = System.currentTimeMillis();
 249  5
         protected String createdBy = createdBy();
 250  5
         protected String metaFilter = "";
 251  
 
 252  5
         private Set<String> meta = new HashSet<String>();
 253  5
         private List<XRefStory> stories = new ArrayList<XRefStory>();
 254  5
         private List<StepMatch> stepMatches = new ArrayList<StepMatch>();
 255  
 
 256  
         private transient boolean excludeStoriesWithNoExecutedScenarios;
 257  
 
 258  5
         public XRefRoot() {
 259  5
         }
 260  
 
 261  
 
 262  
         public void setExcludeStoriesWithNoExecutedScenarios(boolean exclude) {
 263  5
             this.excludeStoriesWithNoExecutedScenarios = exclude;
 264  5
         }
 265  
 
 266  
         protected String createdBy() {
 267  5
             return "JBehave";
 268  
         }
 269  
 
 270  
         protected void processStories(List<StoryHolder> stories, Set<String> stepsPerformed, Map<String, Long> times, StoryReporterBuilder builder, Set<String> failures) {
 271  
             // Prevent Concurrent Modification Exception.
 272  5
             synchronized (this) {
 273  5
                 for (StoryHolder storyHolder : stories) {
 274  4
                     Story story = storyHolder.story;
 275  4
                     String path = story.getPath();
 276  4
                     if (!path.equals("BeforeStories") && !path.equals("AfterStories")) {
 277  4
                         if (someScenarios(story, stepsPerformed) || !excludeStoriesWithNoExecutedScenarios) {
 278  4
                             XRefStory xRefStory = createXRefStory(builder, story, !failures.contains(path), this);
 279  4
                             xRefStory.started = storyHolder.when;
 280  4
                             xRefStory.duration = getTime(times, story);
 281  4
                             this.stories.add(xRefStory);
 282  
                         }
 283  
                     }
 284  
 
 285  4
                 }
 286  5
             }
 287  5
         }
 288  
 
 289  
         protected Long getTime(Map<String, Long> times, Story story) {
 290  4
             Long time = times.get(story.getPath());
 291  4
             if (time == null) {
 292  0
                 return 0L;
 293  
             }
 294  4
             return time;
 295  
         }
 296  
 
 297  
         protected boolean someScenarios(Story story, Set<String> stepsPerformed) {
 298  4
             return stepsPerformed.contains(story.getPath());
 299  
         }
 300  
 
 301  
         /**
 302  
          * Ensure that XRefStory is instantiated completely, before secondary
 303  
          * methods are invoked (or overridden)
 304  
          */
 305  
         protected final XRefStory createXRefStory(StoryReporterBuilder storyReporterBuilder, Story story,
 306  
                 boolean passed, XRefRoot root) {
 307  4
             XRefStory xrefStory = createXRefStory(storyReporterBuilder, story, passed);
 308  4
             xrefStory.processMetaTags(root);
 309  4
             xrefStory.processScenarios();
 310  4
             return xrefStory;
 311  
         }
 312  
 
 313  
         /**
 314  
          * Override this is you want to add fields to the JSON. Specifically,
 315  
          * create a subclass of XRefStory to return.
 316  
          * 
 317  
          * @param storyReporterBuilder the story reporter builder
 318  
          * @param story the story
 319  
          * @param passed the story passed (or failed)
 320  
          * @return An XRefStory
 321  
          */
 322  
         protected XRefStory createXRefStory(StoryReporterBuilder storyReporterBuilder, Story story, boolean passed) {
 323  3
             return new XRefStory(story, storyReporterBuilder, passed);
 324  
         }
 325  
 
 326  
         protected void addStepMatches(Map<String, StepMatch> stepMatchMap) {
 327  5
             for (String key : stepMatchMap.keySet()) {
 328  4
                 StepMatch stepMatch = stepMatchMap.get(key);
 329  4
                 stepMatches.add(stepMatch);
 330  4
             }
 331  5
         }
 332  
     }
 333  
 
 334  
     @SuppressWarnings("unused")
 335  
     public static class XRefStory {
 336  
         private transient Story story; // don't turn into JSON.
 337  
         private String description;
 338  4
         private String narrative = "";
 339  
         private String name;
 340  
         private String path;
 341  
         private String html;
 342  4
         private String meta = "";
 343  4
         private String scenarios = "";
 344  
         private boolean passed;
 345  
         public long started;
 346  
         public long duration;
 347  
 
 348  4
         public XRefStory(Story story, StoryReporterBuilder storyReporterBuilder, boolean passed) {
 349  4
             this.story = story;
 350  4
             Narrative narrative = story.getNarrative();
 351  4
             if (!narrative.isEmpty()) {
 352  4
                 this.narrative = "In order to " + narrative.inOrderTo() + "\n" + "As a " + narrative.asA() + "\n"
 353  
                         + "I want to " + narrative.iWantTo() + "\n";
 354  
             }
 355  4
             this.description = story.getDescription().asString();
 356  4
             this.name = story.getName();
 357  4
             this.path = story.getPath();
 358  4
             this.passed = passed;
 359  4
             this.html = storyReporterBuilder.pathResolver().resolveName(new StoryLocation(storyReporterBuilder.codeLocation(), story.getPath()),
 360  
                     "html");
 361  4
         }
 362  
 
 363  
         protected void processScenarios() {
 364  4
             for (Scenario scenario : story.getScenarios()) {
 365  0
                 String body = "Scenario:" + scenario.getTitle() + "\n";
 366  0
                 List<String> steps = scenario.getSteps();
 367  0
                 for (String step : steps) {
 368  0
                     body = body + step + "\n";
 369  
                 }
 370  0
                 scenarios = scenarios + body + "\n\n";
 371  0
             }
 372  4
         }
 373  
 
 374  
         protected void processMetaTags(XRefRoot root) {
 375  4
             Meta storyMeta = story.getMeta();
 376  4
             for (String next : storyMeta.getPropertyNames()) {
 377  8
                 String property = next + "=" + storyMeta.getProperty(next);
 378  8
                 addMetaProperty(property, root.meta);
 379  8
                 String newMeta = appendMetaProperty(property, this.meta);
 380  8
                 if (newMeta != null) {
 381  7
                     this.meta = newMeta;
 382  
                 }
 383  8
             }
 384  4
         }
 385  
 
 386  
         protected String appendMetaProperty(String property, String meta) {
 387  7
             return meta + property + "\n";
 388  
         }
 389  
 
 390  
         protected void addMetaProperty(String property, Set<String> meta) {
 391  7
             meta.add(property);
 392  7
         }
 393  
     }
 394  
 
 395  
     @SuppressWarnings("unused")
 396  
     public static class StepUsage {
 397  
         private final String story;
 398  
         private final String scenario;
 399  
         private final String step;
 400  
 
 401  4
         public StepUsage(String story, String scenario, String step) {
 402  4
             this.story = story;
 403  4
             this.scenario = scenario;
 404  4
             this.step = step;
 405  4
         }
 406  
     }
 407  
 
 408  4
     public static class StepMatch {
 409  
         private final StepType type; // key
 410  
         private final String annotatedPattern; // key
 411  
         // these not in hashcode or equals()
 412  
         @SuppressWarnings("unused")
 413  
         private final String resolvedPattern;
 414  4
         private final Set<StepUsage> usages = new HashSet<StepUsage>();
 415  
 
 416  4
         public StepMatch(StepType type, String annotatedPattern, String resolvedPattern) {
 417  4
             this.type = type;
 418  4
             this.annotatedPattern = annotatedPattern;
 419  4
             this.resolvedPattern = resolvedPattern;
 420  4
         }
 421  
 
 422  
         @Override
 423  
         public boolean equals(Object o) {
 424  0
             if (this == o)
 425  0
                 return true;
 426  0
             if (o == null || getClass() != o.getClass())
 427  0
                 return false;
 428  
 
 429  0
             StepMatch stepMatch = (StepMatch) o;
 430  
 
 431  0
             if (annotatedPattern != null ? !annotatedPattern.equals(stepMatch.annotatedPattern)
 432  
                     : stepMatch.annotatedPattern != null)
 433  0
                 return false;
 434  0
             if (type != stepMatch.type)
 435  0
                 return false;
 436  
 
 437  0
             return true;
 438  
         }
 439  
 
 440  
         @Override
 441  
         public int hashCode() {
 442  0
             int result = type != null ? type.hashCode() : 0;
 443  0
             result = 31 * result + (annotatedPattern != null ? annotatedPattern.hashCode() : 0);
 444  0
             return result;
 445  
         }
 446  
     }
 447  
 
 448  4
     private class StoryHolder {
 449  
         Story story;
 450  
         long when;
 451  
 
 452  4
         private StoryHolder(Story story) {
 453  4
             this.story = story;
 454  4
             this.when = System.currentTimeMillis();
 455  4
         }
 456  
 
 457  
 
 458  
     }
 459  
 }