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