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