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